@stravica-ai/digital-operator-pa 0.1.40 → 0.1.41

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 (83) hide show
  1. package/README.md +5 -0
  2. package/bin/confluence +9 -0
  3. package/bin/confluence-fetch +9 -0
  4. package/bin/jira +9 -0
  5. package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
  6. package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
  7. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
  8. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
  9. package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
  10. package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
  11. package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
  12. package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
  13. package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
  14. package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
  15. package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
  16. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
  17. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
  18. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
  19. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
  20. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
  21. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
  22. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
  23. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
  24. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
  25. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
  26. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
  27. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
  28. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
  29. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
  30. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
  31. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
  32. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
  33. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
  34. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
  35. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
  36. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
  37. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
  38. package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
  39. package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
  40. package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
  41. package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
  42. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
  43. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
  44. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
  45. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
  46. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
  47. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
  48. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
  49. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
  50. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
  51. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
  52. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
  53. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
  54. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
  55. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
  56. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
  57. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
  58. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
  59. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
  60. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
  61. package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
  62. package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
  63. package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
  64. package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
  65. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
  66. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
  67. package/dist/engineering-pa-scaffold/README.md +32 -1
  68. package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
  69. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
  70. package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
  71. package/dist/engineering-pa-scaffold/src/generator.mjs +104 -2
  72. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +5 -5
  73. package/dist/engineering-pa-types-engineering/index.mjs +14 -6
  74. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
  75. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
  76. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
  77. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
  78. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
  79. package/dist/engineering-pa-types-engineering/package.json +2 -2
  80. package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
  81. package/docs/onboarding-solo.md +55 -4
  82. package/docs/what-it-can-do.md +85 -0
  83. package/package.json +5 -2
package/README.md CHANGED
@@ -20,6 +20,11 @@ New to the platform and setting up a single-person PA on one laptop? Follow
20
20
  ten-minute walkthrough from an empty directory to a working PA under your
21
21
  own Claude Code subscription.
22
22
 
23
+ Want a plain-language tour of what the PA does before you install it? Read
24
+ [`./docs/what-it-can-do.md`](./docs/what-it-can-do.md) — a day-to-day view
25
+ of the capabilities, how it learns you, and what is (and is not) in this
26
+ release.
27
+
23
28
  ## Bins
24
29
 
25
30
  The wrapper exposes the platform's operator-facing CLIs:
package/bin/confluence ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // confluence - Confluence Cloud CLI (v1 CQL + v2 CRUD). Shares JIRA_* creds.
3
+ // Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
4
+ import('../dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs').then((m) => {
5
+ return m.main(process.argv.slice(2));
6
+ }).catch((err) => {
7
+ process.stderr.write(`[confluence] ${err?.message ?? err}\n`);
8
+ process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
9
+ });
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // confluence-fetch - search + fetch + save Confluence pages into operator/notes/.
3
+ // Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
4
+ import('../dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs').then((m) => {
5
+ return m.main(process.argv.slice(2));
6
+ }).catch((err) => {
7
+ process.stderr.write(`[confluence-fetch] ${err?.message ?? err}\n`);
8
+ process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
9
+ });
package/bin/jira ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // jira - Jira REST v3 CLI. Auth reads env vars first, else `secrets get JIRA_*`.
3
+ // Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
4
+ import('../dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs').then((m) => {
5
+ return m.main(process.argv.slice(2));
6
+ }).catch((err) => {
7
+ process.stderr.write(`[jira] ${err?.message ?? err}\n`);
8
+ process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
9
+ });
@@ -0,0 +1,62 @@
1
+ <!--
2
+ scaffold: workspace-extensions.md
3
+ bucket: platform-tooling (Bucket 7)
4
+ audience: the PA, deciding when to propose that the operator build a workspace page.
5
+ what belongs here:
6
+ - placement rule (common-use CLIs in core, personal pages as extensions).
7
+ - triggers the PA watches for.
8
+ - what to offer, and what NOT to prompt for.
9
+ - how to prompt.
10
+ - cross-link to the workspace README for the mechanism.
11
+ -->
12
+
13
+ # Workspace extensions: when to propose one
14
+
15
+ ## Placement rule
16
+
17
+ Common-use CLIs live in core. Personal workspace pages are a more personal expression of how the operator works, so they belong in an extension. The PA carries the knowledge that decides when to prompt the operator to build one.
18
+
19
+ The mechanism itself is already live. Static page overlay under `operator/extensions/workspace/<name>.html`, optional API routes via `operator/extensions/workspace/api.mjs` (exporting `register(addRoute)`), nav registration via `operator/extensions/workspace/nav.json` (or team-scoped `nav.<team>.json` fragments). Full detail: `@stravica-ai/engineering-pa-workspace/README.md` (extension mechanism section).
20
+
21
+ ## Triggers
22
+
23
+ Three shapes of operator behaviour that mean a workspace page would pay for itself.
24
+
25
+ 1. **Repeated manual view.** The operator opens the same files, runs the same query, or screenshots the same panel more than twice in a week. The manual view is already a page; it just does not exist yet.
26
+ 2. **Explicit ask for a dashboard or "one page to see X".** Any request framed as "I want to see all my X in one place", "give me a page for Y", "I want to check Z at a glance". The operator has done the design work already.
27
+ 3. **An operator-installed tool with a natural page.** A Jira CLI plus a "my Jira today" habit is the archetype. Any tool the operator installed and now reaches for daily has a page shape latent in it.
28
+
29
+ ## What to offer
30
+
31
+ Match the surface to the shape.
32
+
33
+ - **Read-only view of files or state on disk.** Static page overlay. HTML file at `operator/extensions/workspace/<name>.html` served in place of a core same-named page, or as a new page at `/<name>.html`.
34
+ - **Anything that needs server access or a write.** Static page plus an API route in `operator/extensions/workspace/api.mjs`. The route hangs off `register(addRoute)` and lands under `/__api/<name>/<verb>`.
35
+ - **Discoverability in the sidebar.** Add a `nav.json` entry naming the page. If the operator runs multiple teams on the same box, use `nav.<team>.json` fragments so each team's entries survive the library merge.
36
+
37
+ ## What NOT to prompt for
38
+
39
+ Not every operator habit is a workspace page.
40
+
41
+ - **One-shot views.** A page for a single question the operator asked once is over-engineering. The answer belongs in the chat, or in a `viewer.html?file=...` link.
42
+ - **Throwaway reports.** If the output has a shelf-life of one day, a workspace page is the wrong home. Stage the file, link it, move on.
43
+ - **Anything that is a workspace page and a knowledge doc and a scheduled task rolled into one.** That is a tool. Propose `tool-developer` under Bucket 7 rule 9, not a workspace extension.
44
+
45
+ ## How to prompt
46
+
47
+ One sentence. Name the page, the source of data, and the interaction shape.
48
+
49
+ > "Want me to build a workspace page for X? Reads from Y, refresh on nav-load, no writes."
50
+
51
+ Get an explicit yes before doing anything durable. On approval, either:
52
+
53
+ - Invoke `pa-init workspace-extension <name>` for the file skeleton plus nav entry (add `--with-api` if the page needs a server route), then dispatch `tool-developer` for the real page content and any handler logic, OR
54
+ - Skip the scaffold verb and dispatch `tool-developer` directly if the shape is unusual enough that the skeleton would just be deleted.
55
+
56
+ The scaffold verb is a thirty-second cost saver; it does not replace the design conversation.
57
+
58
+ ## Related
59
+
60
+ - CLAUDE.md.template Bucket 7 rule 9 (proactive tool proposal): the parent rule; workspace pages count as a tool for that rule.
61
+ - `@stravica-ai/engineering-pa-workspace/README.md`: the extension mechanism, page overlay, API-route registration, nav merge, team scoping.
62
+ - `packages/core/knowledge/platform-tooling/workspace-server.md`: workspace server operations (start, stop, viewer.html).
@@ -1,31 +1,96 @@
1
1
  # @stravica-ai/engineering-pa-core-extensions-atlassian
2
2
 
3
- Identity: `@stravica-ai/engineering-pa-core-extensions-atlassian` — optional Atlassian (Jira/Confluence) core extension.
3
+ Atlassian (Jira, Confluence) CLIs and library surface, promoted into core
4
+ for the alpha solo scaffold. Common-use tools ship with the platform; the
5
+ personal workspace pages colleagues build on top ship as extensions.
4
6
 
5
- ## Status
7
+ ## Bins
6
8
 
7
- **Skeleton — v0.1.0.** Workspace-internal, `"private": true`, not individually published.
9
+ Wrapped by `@stravica-ai/digital-operator-pa` and installed on `PATH` when
10
+ that package is installed:
8
11
 
9
- Phase 2 promotes the existing operator-side Atlassian extension(s) into core via the extension-promotion mechanism: the upgrade tool detects a local extension matching this namespace, deactivates the local copy, copies operator-specific config across, and enables the core version.
12
+ - `jira` - Jira REST v3 CLI. Verbs: `auth`, `mine`, `search`, `issue`,
13
+ `create`, `update`, `attach`, `transition`, `advance`, `cancel`,
14
+ `comment`, `worklog`, `projects`, `boards`, `sprint`, `link`,
15
+ `sync-statuses`, `bulk-transition`, `test`.
16
+ - `confluence` - Confluence Cloud CLI (v1 CQL search, v2 page CRUD).
17
+ Verbs: `auth`, `spaces`, `pages`, `page`, `search`, `create`, `update`,
18
+ `comment`, `label`, `tree`, `recent`, `mine`, `mirror-folder`,
19
+ `mirror-page`, `test`.
20
+ - `confluence-fetch` - convenience wrapper: search + fetch + save a page
21
+ to `operator/notes/` and open the workspace viewer.
10
22
 
11
- For v0.1.0, this is an empty slot so the manifest-toggleable extension model is established from day one.
23
+ Run `jira --help`, `confluence --help`, `confluence-fetch --help` for the
24
+ full verb list; each verb has its own `--help`.
12
25
 
13
- ## Layout
26
+ ## Auth
27
+
28
+ Both CLIs read three credentials from the environment first, else from the
29
+ platform `secrets` bin. The keys are shared because Jira and Confluence
30
+ target the same Atlassian site:
31
+
32
+ - `JIRA_DOMAIN` - subdomain of `atlassian.net` (`foo` for `foo.atlassian.net`)
33
+ - `JIRA_EMAIL` - your Atlassian account email
34
+ - `JIRA_API_TOKEN` - a token from `https://id.atlassian.com/manage-profile/security/api-tokens`
35
+
36
+ Two-step colleague setup, after installing `@stravica-ai/digital-operator-pa`:
14
37
 
15
38
  ```
16
- packages/core-extensions/atlassian/
17
- index.mjs # placeholder export
18
- package.json # name, version, scripts
19
- README.md # this file
20
- test/index.test.mjs
39
+ secrets set JIRA_DOMAIN yoursite
40
+ secrets set JIRA_EMAIL you@example.com
41
+ secrets set JIRA_API_TOKEN <paste-token>
42
+ ```
43
+
44
+ Resolution order for each key:
45
+
46
+ 1. Environment variable of the same name (`JIRA_DOMAIN`, ...).
47
+ 2. `secrets get <prefix>JIRA_DOMAIN` (etc.), where `<prefix>` comes from
48
+ `ATLASSIAN_SECRETS_PREFIX` (e.g. an org slug like `acme/` picks the `acme` vault scope in
49
+ an operator estate; empty by default so a solo colleague's single scope
50
+ just works).
51
+
52
+ The tool never prints or logs the values; child-process reads use
53
+ `stdio: ['ignore', 'pipe', 'pipe']`.
54
+
55
+ ## Configuration
56
+
57
+ - `config/jira-workflow-chains.default.json` - shipped empty. `jira advance
58
+ <KEY> <STATUS>` walks a per-site chain of statuses; override by writing
59
+ `operator/config/jira-workflow-chains.json` in your workspace root.
60
+ - `operator/config/jira-status-mappings.json` - per-project status mapping
61
+ used by `jira sync-statuses`; not shipped by default.
62
+ - `JIRA_DEFAULT_PROJECT` (optional, env or vault key of the same name) -
63
+ when set, `jira mine` narrows to that project unless `--project KEY`
64
+ or `--all` is passed. With nothing configured, `jira mine` returns
65
+ issues across every project the account can see.
66
+
67
+ ## Library surface
68
+
69
+ ```js
70
+ import {
71
+ createJiraClient,
72
+ createConfluenceClient,
73
+ storageToMarkdown,
74
+ } from '@stravica-ai/digital-operator-pa/dist/engineering-pa-core-extensions-atlassian/index.mjs';
75
+
76
+ const jira = createJiraClient();
77
+ const { data } = await jira.get('/rest/api/3/myself');
21
78
  ```
22
79
 
23
- ## Tests
80
+ For tests, inject a stub credential resolver rather than the vault:
24
81
 
25
- ```bash
26
- pnpm --filter @stravica-ai/engineering-pa-core-extensions-atlassian test
82
+ ```js
83
+ const client = createJiraClient({
84
+ domain: 'yoursite',
85
+ email: 'you@example.com',
86
+ apiToken: 'test-token',
87
+ fetchFn: mockFetch,
88
+ });
27
89
  ```
28
90
 
29
- ## Standards
91
+ ## Workspace hook
30
92
 
31
- Follows the [tooling standard](../../../CONTRIBUTING.md#standards): JS only, zero npm deps, `node:test`, camelCase JSON.
93
+ `jira` writes POST to `http://127.0.0.1:<workspacePort>/__api/jira/invalidate`
94
+ after mutating verbs, so a workspace page can refresh cached data on
95
+ change. The endpoint is a workspace-extension concern; the CLI treats a
96
+ timeout or refused connection as fine (the write succeeded regardless).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../src/confluence/cli.mjs').then((m) => m.main(process.argv.slice(2)));
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../src/confluence-fetch/cli.mjs').then((m) => m.main(process.argv.slice(2)));
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../src/jira/cli.mjs').then((m) => m.main(process.argv.slice(2)));
@@ -0,0 +1,4 @@
1
+ {
2
+ "_note": "Per-site Jira workflow chains for `jira advance <KEY> <STATUS>`. Ships empty because every Atlassian instance has its own workflows. Override by writing operator/config/jira-workflow-chains.json in your workspace root; the CLI reads that path first and falls back to this shipped default.",
3
+ "chains": []
4
+ }
@@ -1,9 +1,20 @@
1
- // @stravica-ai/engineering-pa-core-extensions-atlassian — optional Atlassian
2
- // integration (Jira/Confluence tooling + workspace pages).
1
+ // @stravica-ai/engineering-pa-core-extensions-atlassian
3
2
  //
4
- // Status: skeleton. Phase 2 promotes the existing operator-side Atlassian
5
- // extension(s) into core. Until then, this is a placeholder slot so the
6
- // manifest-toggleable extension model is established from day one.
3
+ // Common-use Atlassian CLIs (jira, confluence, confluence-fetch), promoted
4
+ // into core so the alpha solo scaffold ships them out of the box. The bins
5
+ // live under bin/ and are wrapped from @stravica-ai/digital-operator-pa's
6
+ // bin map. Auth reads env vars first, else `secrets get JIRA_*` (see
7
+ // src/common/secrets.mjs).
8
+
9
+ export { createClient as createJiraClient, getCredentials as getJiraCredentials, JiraError } from './src/jira/client.mjs';
10
+ export { createClient as createConfluenceClient, getCredentials as getConfluenceCredentials, ConfluenceError } from './src/confluence/client.mjs';
11
+ export {
12
+ storageToMarkdown,
13
+ markdownToStorage,
14
+ storageToText,
15
+ textToStorage,
16
+ } from './src/confluence/storage.mjs';
17
+ export { loadAtlassianCredentials, resolveSecret, CredentialResolutionError } from './src/common/secrets.mjs';
7
18
 
8
19
  export const name = './';
9
- export const status = 'skeleton';
20
+ export const status = 'active';
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "name": "@stravica-ai/engineering-pa-core-extensions-atlassian",
3
- "version": "0.1.1",
4
- "description": "Atlassian (Jira/Confluence) core extension — optional, manifest-toggleable.",
3
+ "version": "0.2.0",
4
+ "description": "Atlassian (Jira, Confluence, confluence-fetch) CLIs and library surface. Promoted into core for the alpha solo scaffold.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "exports": {
8
- ".": "./index.mjs"
8
+ ".": "./index.mjs",
9
+ "./jira": "./src/jira/client.mjs",
10
+ "./confluence": "./src/confluence/client.mjs"
11
+ },
12
+ "bin": {
13
+ "jira": "bin/jira",
14
+ "confluence": "bin/confluence",
15
+ "confluence-fetch": "bin/confluence-fetch"
9
16
  },
10
17
  "private": true,
11
18
  "license": "MIT",
@@ -15,5 +22,5 @@
15
22
  "scripts": {
16
23
  "test": "node --test --test-isolation=none test/*.test.mjs"
17
24
  },
18
- "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper — see packages/cli/test/secrets. Counts must never lie."
25
+ "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper - see packages/cli/test/secrets. Counts must never lie."
19
26
  }
@@ -0,0 +1,137 @@
1
+ // client.mjs - Shared fail-silent transport for notifying the local
2
+ // workspace server that a CLI tool's backing data changed.
3
+ //
4
+ // Extracted so any Atlassian CLI (jira today; confluence or others later) can
5
+ // fire a lightweight "hey, invalidate your cache" POST at a tool-specific
6
+ // endpoint without re-implementing the fetch/timeout/disable-flag plumbing.
7
+ // Callers bind their own endpoint, body shape, and disable env var (see
8
+ // operator/extensions/tools/jira/src/invalidate.mjs for the reference
9
+ // caller); this module only owns the wire mechanics.
10
+ //
11
+ // Contract (binding on every caller):
12
+ // - Fail-silent. Never throws - always resolves to { ok, error? }. A
13
+ // workspace-notify feature must never block or crash the CLI command
14
+ // whose actual job is mutating the upstream system (Jira, Confluence, ...).
15
+ // - Non-blocking by design. Bounded by timeoutMs (default 500ms) via
16
+ // AbortController; the caller gets an answer promptly either way.
17
+ // - Zero runtime deps. Node 20+ globalThis.fetch + AbortController only.
18
+ //
19
+ // Port resolution: opts.port wins; else the workspacePort from the merged
20
+ // preferences (device-local preferences.local.json over the synced
21
+ // preferences.json); else the platform default (4200) - same convention as
22
+ // operator/extensions/tools/confluence-fetch/src/fetch.mjs.
23
+ //
24
+ // The overlay (preferences.local.json) is the operator's device-scoped
25
+ // escape hatch: gitignored, so a per-device workspacePort never travels
26
+ // through the synced base file. Precedence matches the platform's own
27
+ // reader in engineering-pa-core src/desk/team-scope.mjs mergePreferences().
28
+
29
+ import { resolve } from 'node:path';
30
+ import { readFileSync } from 'node:fs';
31
+
32
+ // Repo root: env override, else cwd. The tool is invoked from a workspace
33
+ // root so cwd is the correct default; keeps preferences.local.json overlay
34
+ // resolution working under a downstream install.
35
+ const REPO_ROOT =
36
+ process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
37
+ const PREFERENCES_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.json');
38
+ const PREFERENCES_LOCAL_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.local.json');
39
+
40
+ const DEFAULT_TIMEOUT_MS = 500;
41
+ const PLATFORM_DEFAULT_PORT = 4200;
42
+
43
+ function readJsonObject(path) {
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
46
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
47
+ } catch {
48
+ // Missing or malformed - that layer contributes nothing.
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Resolve the local workspace port from the merged preferences.
55
+ * Overlay keys win; keys it omits fall through to the synced base; a device
56
+ * with no overlay behaves exactly as before this change.
57
+ *
58
+ * Paths are injectable for tests only - production callers pass nothing.
59
+ *
60
+ * @param {object} [paths]
61
+ * @param {string} [paths.base] preferences.json path.
62
+ * @param {string} [paths.local] preferences.local.json path.
63
+ * @returns {number}
64
+ */
65
+ export function resolveWorkspacePort({ base = PREFERENCES_PATH, local = PREFERENCES_LOCAL_PATH } = {}) {
66
+ const prefs = { ...(readJsonObject(base) || {}), ...(readJsonObject(local) || {}) };
67
+ return typeof prefs.workspacePort === 'number' ? prefs.workspacePort : PLATFORM_DEFAULT_PORT;
68
+ }
69
+
70
+ function resolvePort(overridePort) {
71
+ if (typeof overridePort === 'number') return overridePort;
72
+ return resolveWorkspacePort();
73
+ }
74
+
75
+ /**
76
+ * Fire a fail-silent POST notify against the local workspace server.
77
+ * Always resolves; never throws.
78
+ *
79
+ * @param {object} opts
80
+ * @param {string} opts.endpoint Path to POST to, e.g. '/__api/jira/invalidate'.
81
+ * @param {string} [opts.disableEnv] Env var name; value '0' short-circuits
82
+ * (skips the network call entirely).
83
+ * @param {object} [opts.body] JSON body to send. Defaults to {}.
84
+ * @param {number} [opts.port] Override the resolved workspace port.
85
+ * @param {number} [opts.timeoutMs] Override the abort timeout (default 500ms).
86
+ * @param {Function} [opts.fetchImpl] Override fetch (test injection).
87
+ * @returns {Promise<{ok: boolean, error?: string}>}
88
+ */
89
+ export async function notifyInvalidate(opts = {}) {
90
+ const { endpoint, disableEnv, body = {}, timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl } = opts;
91
+
92
+ if (disableEnv && process.env[disableEnv] === '0') {
93
+ return { ok: false, error: 'disabled' };
94
+ }
95
+
96
+ const fetchFn = typeof fetchImpl === 'function' ? fetchImpl : (fetchImpl || globalThis.fetch);
97
+ if (typeof fetchFn !== 'function') {
98
+ return { ok: false, error: 'fetch unavailable' };
99
+ }
100
+
101
+ const port = resolvePort(opts.port);
102
+ const url = `http://127.0.0.1:${port}${endpoint}`;
103
+
104
+ const controller = new AbortController();
105
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
106
+ try {
107
+ const res = await fetchFn(url, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify(body),
111
+ signal: controller.signal,
112
+ });
113
+ if (!res.ok) {
114
+ return { ok: false, error: `http ${res.status}` };
115
+ }
116
+ return { ok: true };
117
+ } catch (err) {
118
+ return { ok: false, error: err && err.message ? err.message : String(err) };
119
+ } finally {
120
+ clearTimeout(timer);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Fire-and-forget wrapper. Returns immediately usable promise that never
126
+ * rejects, even if the caller doesn't await it - safe to call without a
127
+ * trailing .catch().
128
+ *
129
+ * @param {object} [opts] Same shape as notifyInvalidate.
130
+ * @returns {Promise<{ok: boolean, error?: string}>}
131
+ */
132
+ export function notifyInvalidateAsync(opts = {}) {
133
+ return notifyInvalidate(opts).catch((err) => ({
134
+ ok: false,
135
+ error: err && err.message ? err.message : String(err),
136
+ }));
137
+ }
@@ -0,0 +1,58 @@
1
+ // pa-name.mjs - Resolve the configured PA name for colleague-visible
2
+ // artefacts (e.g. the marker prefix on Jira comments the CLIs post).
3
+ //
4
+ // Precedence:
5
+ // 1. env PA_NAME - explicit per-shell override, wins outright.
6
+ // 2. paName in the operator preferences (device-local
7
+ // preferences.local.json overlay over the synced preferences.json) -
8
+ // the scaffold writes this at pa-init time; it matches the
9
+ // `agents/<paName>/` directory shape.
10
+ // 3. `'pa'` - a generic fallback so the marker is never blank.
11
+ //
12
+ // The reader mirrors invalidate-client.mjs on repo-root resolution and
13
+ // preferences layering so operators get one story.
14
+
15
+ import { resolve } from 'node:path';
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const REPO_ROOT =
19
+ process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
20
+ const PREFERENCES_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.json');
21
+ const PREFERENCES_LOCAL_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.local.json');
22
+
23
+ const DEFAULT_PA_NAME = 'pa';
24
+
25
+ function readJsonObject(path) {
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
28
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function isNonEmptyString(v) {
35
+ return typeof v === 'string' && v.trim().length > 0;
36
+ }
37
+
38
+ /**
39
+ * Resolve the configured PA name.
40
+ *
41
+ * Paths are injectable for tests only - production callers pass nothing.
42
+ *
43
+ * @param {object} [opts]
44
+ * @param {string} [opts.envValue] Override the PA_NAME env value (test injection).
45
+ * @param {string} [opts.base] preferences.json path.
46
+ * @param {string} [opts.local] preferences.local.json path.
47
+ * @returns {string}
48
+ */
49
+ export function resolvePaName({
50
+ envValue = process.env.PA_NAME,
51
+ base = PREFERENCES_PATH,
52
+ local = PREFERENCES_LOCAL_PATH,
53
+ } = {}) {
54
+ if (isNonEmptyString(envValue)) return envValue.trim();
55
+ const prefs = { ...(readJsonObject(base) || {}), ...(readJsonObject(local) || {}) };
56
+ if (isNonEmptyString(prefs.paName)) return prefs.paName.trim();
57
+ return DEFAULT_PA_NAME;
58
+ }
@@ -0,0 +1,158 @@
1
+ // secrets.mjs - resolve Atlassian credentials for the jira and confluence
2
+ // CLIs (and the shared client factories in src/jira, src/confluence).
3
+ //
4
+ // Both CLIs point at the same Atlassian site so the three required keys are
5
+ // shared:
6
+ //
7
+ // JIRA_DOMAIN Atlassian subdomain, e.g. "yoursite" for yoursite.atlassian.net
8
+ // JIRA_EMAIL Account email used with the API token
9
+ // JIRA_API_TOKEN User-scoped API token from id.atlassian.com
10
+ //
11
+ // One optional key is also resolved on demand (never required):
12
+ //
13
+ // JIRA_DEFAULT_PROJECT Project key the `jira mine` verb narrows to when
14
+ // the operator does not pass --project / --all. With
15
+ // nothing configured, `mine` returns issues across
16
+ // every project the account can see.
17
+ //
18
+ // Resolution order for each key:
19
+ // 1. Explicit override via opts.secretsGet(key) (tests inject a function)
20
+ // 2. Environment variable of the same name (JIRA_DOMAIN, ...)
21
+ // 3. Platform `secrets` bin: `secrets get <ATLASSIAN_SECRETS_PREFIX?>KEY`
22
+ // Runs against the workspace-root vault the operator scaffold seeds.
23
+ // A prefix such as an org-slug like `acme/` (env var
24
+ // ATLASSIAN_SECRETS_PREFIX) picks a non-default scope via the platform
25
+ // full-scope reference form.
26
+ //
27
+ // The tool never prints or logs the values. All read paths run
28
+ // `stdio: ['ignore', 'pipe', 'pipe']` and the caller's error strings never
29
+ // interpolate the value.
30
+ //
31
+ // Zero npm dependencies. Uses node:child_process for the fallback bin call.
32
+
33
+ import { execFileSync } from 'node:child_process';
34
+
35
+ /**
36
+ * Sentinel raised when a required credential cannot be resolved. Callers
37
+ * wrap this in their own JiraError / ConfluenceError with exit code 3.
38
+ */
39
+ export class CredentialResolutionError extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = 'CredentialResolutionError';
43
+ }
44
+ }
45
+
46
+ const KEYS = ['JIRA_DOMAIN', 'JIRA_EMAIL', 'JIRA_API_TOKEN'];
47
+
48
+ /**
49
+ * Resolve a single Atlassian secret. Not for external use; the CLIs call
50
+ * loadAtlassianCredentials.
51
+ * @param {string} key
52
+ * @param {{ secretsGet?: (key: string) => string, secretsBin?: string, env?: Record<string,string> }} opts
53
+ * @returns {string}
54
+ */
55
+ export function resolveSecret(key, opts = {}) {
56
+ if (typeof opts.secretsGet === 'function') {
57
+ const v = opts.secretsGet(key);
58
+ if (typeof v === 'string' && v.length > 0) return v.trim();
59
+ throw new CredentialResolutionError(`opts.secretsGet returned no value for ${key}`);
60
+ }
61
+
62
+ const env = opts.env ?? process.env;
63
+
64
+ const envValue = env[key];
65
+ if (typeof envValue === 'string' && envValue.length > 0) return envValue.trim();
66
+
67
+ const prefix = env.ATLASSIAN_SECRETS_PREFIX ?? '';
68
+ const scopedKey = `${prefix}${key}`;
69
+ const bin = opts.secretsBin ?? 'secrets';
70
+
71
+ try {
72
+ const out = execFileSync(bin, ['get', scopedKey], {
73
+ encoding: 'utf8',
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ env,
76
+ });
77
+ const trimmed = out.trim();
78
+ if (trimmed.length === 0) {
79
+ throw new CredentialResolutionError(
80
+ `platform \`secrets get ${scopedKey}\` returned empty output`,
81
+ );
82
+ }
83
+ return trimmed;
84
+ } catch (err) {
85
+ if (err instanceof CredentialResolutionError) throw err;
86
+ const stderrSnippet = err.stderr ? String(err.stderr).trim().split('\n').pop() : '';
87
+ const hint = stderrSnippet
88
+ ? ` (${stderrSnippet})`
89
+ : ' (bin missing? try `pnpm exec secrets get` from your operator repo)';
90
+ throw new CredentialResolutionError(
91
+ `could not read ${key} from env or \`secrets get ${scopedKey}\`${hint}`,
92
+ );
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve an OPTIONAL Atlassian setting. Same env -> secrets bin order as
98
+ * resolveSecret, but returns null instead of throwing when the key is
99
+ * absent so a caller can fall back to a "no default" branch cleanly. Used
100
+ * for JIRA_DEFAULT_PROJECT and any other future soft config the tools
101
+ * accept but never require.
102
+ *
103
+ * A missing env var and a bin call that exits non-zero, both return null;
104
+ * an empty string from the bin returns null too. Any other exec failure
105
+ * (bin missing, permission error) is swallowed so an operator without the
106
+ * `secrets` bin on PATH still gets the "no default" behaviour rather than
107
+ * a crash on an optional lookup. The primary credential path stays strict.
108
+ *
109
+ * @param {string} key
110
+ * @param {{ secretsGet?: (key: string) => string, secretsBin?: string, env?: Record<string,string> }} opts
111
+ * @returns {string | null}
112
+ */
113
+ export function resolveOptionalSecret(key, opts = {}) {
114
+ if (typeof opts.secretsGet === 'function') {
115
+ try {
116
+ const v = opts.secretsGet(key);
117
+ if (typeof v === 'string' && v.length > 0) return v.trim();
118
+ return null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ const env = opts.env ?? process.env;
125
+
126
+ const envValue = env[key];
127
+ if (typeof envValue === 'string' && envValue.length > 0) return envValue.trim();
128
+
129
+ const prefix = env.ATLASSIAN_SECRETS_PREFIX ?? '';
130
+ const scopedKey = `${prefix}${key}`;
131
+ const bin = opts.secretsBin ?? 'secrets';
132
+
133
+ try {
134
+ const out = execFileSync(bin, ['get', scopedKey], {
135
+ encoding: 'utf8',
136
+ stdio: ['ignore', 'pipe', 'pipe'],
137
+ env,
138
+ });
139
+ const trimmed = out.trim();
140
+ return trimmed.length > 0 ? trimmed : null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Load all three Atlassian credentials. Test suites pass in
148
+ * `{ secretsGet: (key) => literal }` to skip both env vars and the bin.
149
+ *
150
+ * @param {{ secretsGet?: (key: string) => string, secretsBin?: string, env?: Record<string,string> }} opts
151
+ * @returns {{ domain: string, email: string, apiToken: string }}
152
+ */
153
+ export function loadAtlassianCredentials(opts = {}) {
154
+ const domain = resolveSecret('JIRA_DOMAIN', opts);
155
+ const email = resolveSecret('JIRA_EMAIL', opts);
156
+ const apiToken = resolveSecret('JIRA_API_TOKEN', opts);
157
+ return { domain, email, apiToken };
158
+ }