overlord-cli 0.2606021051.0 → 0.2606041130.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.
package/README.md CHANGED
@@ -89,7 +89,7 @@ ovld doctor
89
89
 
90
90
  **Antigravity:** Gemini CLI is deprecated. Use `ovld setup antigravity` and `ovld launch antigravity --ticket-id <ticket_id>`. Antigravity manages model selection internally.
91
91
 
92
- For ticket-scoped protocol and launch commands, `ticket_id` values such as `1:899` carry the organization id. The CLI uses that first, then `--organization-id` for UUID compatibility, then stored auth. `ovld auth login` stores the first available organization as a default when an account belongs to multiple organizations; pass `--organization-id <id>` only when you want a different default.
92
+ The CLI is organization-agnostic: `ovld auth login` stores your identity only and never a default organization. Your identity (OAuth session or agent token) resolves to the list of organizations you belong to — see `ovld auth status --verbose`. Each command resolves its organization from the `ticket_id` (values such as `1:899` carry the organization id), then an explicit `--organization-id`, otherwise from your membership. Commands that browse or search — `ovld add-cwd`, `ovld create`/`ovld prompt`, `ovld tickets list`, and `ovld protocol search-tickets` — span every organization you belong to. Actions against an organization you are not a member of return a clear permission error rather than silently falling back to another org.
93
93
 
94
94
  When `ovld launch` has an explicit `--working-directory`, or when you run it from a registered project directory containing `.overlord/project.json`, it exports `TMPDIR`, `TMP`, `TEMP`, and `OVERLORD_TMPDIR` to that project's `.overlord/tmp/` directory before spawning the agent. Otherwise it falls back to the system temp directory.
95
95
 
@@ -12,7 +12,7 @@ import { stdin as input, stdout as output } from 'node:process';
12
12
 
13
13
  import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
14
14
  import { upsertLocalOverlordConfig } from './local-config.mjs';
15
- import { parseNumberedSelection, sortProjects } from './new-ticket.mjs';
15
+ import { fetchProjectsAcrossOrganizations, parseNumberedSelection } from './new-ticket.mjs';
16
16
  import { readOrCreateDeviceFingerprint } from './runner.mjs';
17
17
 
18
18
  function parseFlags(args) {
@@ -41,19 +41,6 @@ function projectLabel(project) {
41
41
  return organizationName ? `${project.name} — ${organizationName}` : project.name;
42
42
  }
43
43
 
44
- async function fetchProjects(platformUrl, bearerToken, localSecret, organizationId) {
45
- const res = await fetch(`${platformUrl}/api/protocol/projects`, {
46
- headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
47
- });
48
- const data = await res.json().catch(() => ({}));
49
- if (!res.ok) {
50
- throw new Error(
51
- `Failed to list projects (${res.status}): ${data.error ?? JSON.stringify(data)}`
52
- );
53
- }
54
- return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
55
- }
56
-
57
44
  async function promptForProject(projects, cwd) {
58
45
  if (!projects.length) {
59
46
  throw new Error('No projects available. Create a project in Overlord first.');
@@ -118,9 +105,10 @@ By default the registered resource is marked primary for this device. Pass
118
105
 
119
106
  const isPrimary = !(flags.primary === 'false' || flags.primary === false);
120
107
 
121
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth({});
108
+ const auth = await resolveAuth({});
109
+ const { platformUrl, bearerToken, localSecret } = auth;
122
110
 
123
- const projects = await fetchProjects(platformUrl, bearerToken, localSecret, organizationId);
111
+ const projects = await fetchProjectsAcrossOrganizations(auth);
124
112
  let project = null;
125
113
  if (typeof flags['project-id'] === 'string' && flags['project-id'].trim()) {
126
114
  const wanted = flags['project-id'].trim();
@@ -139,6 +127,10 @@ By default the registered resource is marked primary for this device. Pass
139
127
 
140
128
  const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
141
129
 
130
+ // Register the resource in the organization that owns the chosen project,
131
+ // never a stored default org.
132
+ const organizationId = project.organizationId ?? null;
133
+
142
134
  const body = {
143
135
  projectId: project.id,
144
136
  directoryPath,
@@ -9,7 +9,7 @@
9
9
  * ovld attach <ticketId> <agent> # non-interactive: launch immediately
10
10
  */
11
11
 
12
- import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
12
+ import { resolveAuth, searchTicketsAcrossOrganizations } from './credentials.mjs';
13
13
  import { runLauncherCommand } from './launcher.mjs';
14
14
 
15
15
  const AGENTS = ['claude', 'cursor', 'codex', 'antigravity', 'opencode'];
@@ -58,26 +58,15 @@ function statusColor(status) {
58
58
 
59
59
  // ─── API ──────────────────────────────────────────────────────────────────────
60
60
 
61
- async function searchTickets(platformUrl, bearerToken, localSecret, organizationId, query) {
62
- const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
63
- method: 'POST',
64
- headers: {
65
- ...buildAuthHeaders(bearerToken, localSecret, organizationId),
66
- 'Content-Type': 'application/json'
67
- },
68
- body: JSON.stringify({
69
- includeCompleted: false,
70
- query,
71
- limit: MAX_VISIBLE
72
- })
61
+ // Organization-agnostic ticket search: spans every organization the identity
62
+ // belongs to (no default org) by delegating to the shared fan-out helper.
63
+ async function searchTickets(auth, query) {
64
+ const { tickets } = await searchTicketsAcrossOrganizations(auth, {
65
+ includeCompleted: false,
66
+ query,
67
+ limit: MAX_VISIBLE
73
68
  });
74
-
75
- if (!res.ok) {
76
- throw new Error(`Failed to search tickets (${res.status}): ${await res.text()}`);
77
- }
78
-
79
- const data = await res.json();
80
- return data.tickets ?? [];
69
+ return tickets ?? [];
81
70
  }
82
71
 
83
72
  // ─── Interactive prompt ───────────────────────────────────────────────────────
@@ -287,7 +276,7 @@ function runInteractivePrompt({ label, items = [], search, prefix = '' }) {
287
276
  export async function runAttachCommand(args) {
288
277
  const [ticketIdArg, agentArg] = args;
289
278
 
290
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
279
+ const auth = await resolveAuth();
291
280
 
292
281
  // ── Phase 1: Ticket selection ──────────────────────────────────────────────
293
282
 
@@ -301,8 +290,7 @@ export async function runAttachCommand(args) {
301
290
 
302
291
  const selectedTicket = await runInteractivePrompt({
303
292
  label: 'Search tickets',
304
- search: nextQuery =>
305
- searchTickets(platformUrl, bearerToken, localSecret, organizationId, nextQuery)
293
+ search: nextQuery => searchTickets(auth, nextQuery)
306
294
  });
307
295
 
308
296
  if (!selectedTicket) {
package/bin/_cli/auth.mjs CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  loadCredentials,
14
14
  loadRuntime,
15
15
  repairCredentials,
16
+ resolveAuth,
17
+ resolveOrganizations,
16
18
  saveCredentials
17
19
  } from './credentials.mjs';
18
20
 
@@ -558,47 +560,91 @@ export async function authLogin(args = []) {
558
560
  credentials.access_token,
559
561
  localSecret
560
562
  );
561
- const selectedOrganization = selectLoginOrganization(organizations, preselectedOrganizationId);
563
+
564
+ // The CLI is organization-agnostic: login stores the identity only, never a
565
+ // default organization. `--organization-id` is still validated so a typo is
566
+ // caught early, but it is no longer persisted — every command resolves its org
567
+ // from the ticket id, an explicit --organization-id, or your membership.
568
+ if (preselectedOrganizationId !== null) {
569
+ selectLoginOrganization(organizations, preselectedOrganizationId);
570
+ }
562
571
 
563
572
  saveCredentials({
564
573
  access_token: credentials.access_token,
565
574
  access_token_expires_at: credentials.access_token_expires_at ?? undefined,
566
575
  refresh_token: credentials.refresh_token,
567
- organization_id: selectedOrganization.id,
568
576
  platform_url: resolvedPlatformUrl
569
577
  });
570
578
 
571
- if (organizations.length > 1 && preselectedOrganizationId === null) {
579
+ console.log('Logged in successfully.');
580
+ printOrganizationMemberships(organizations);
581
+ if (preselectedOrganizationId !== null) {
572
582
  console.log(
573
- `Logged in successfully. Default organization: ${describeOrganization(selectedOrganization)}. ` +
574
- 'Ticket-scoped commands override this from the ticket id; use --organization-id to set a different default.'
583
+ `\nNote: --organization-id is no longer stored as a default. ` +
584
+ 'Commands resolve their organization from the ticket id, an explicit --organization-id, or your membership.'
575
585
  );
576
- } else {
577
- console.log(`Logged in successfully. Default organization: ${describeOrganization(selectedOrganization)}.`);
578
586
  }
579
587
  }
580
588
 
589
+ function printOrganizationMemberships(organizations) {
590
+ if (!organizations.length) {
591
+ console.log(' You are not yet a member of any organization. Complete onboarding in Overlord first.');
592
+ return;
593
+ }
594
+
595
+ if (organizations.length === 1) {
596
+ console.log(` Organization: ${describeOrganization(organizations[0])}`);
597
+ return;
598
+ }
599
+
600
+ console.log(` Member of ${organizations.length} organizations:`);
601
+ for (const organization of organizations) {
602
+ console.log(` - ${describeOrganization(organization)}`);
603
+ }
604
+ console.log(' Commands resolve their organization from the ticket id or --organization-id.');
605
+ }
606
+
581
607
  async function authLoginWithAgentToken(agentToken, args) {
582
608
  const preselectedOrganizationId = parseOrganizationFlag(args);
583
609
  const storedCredentials = loadCredentials();
584
610
  const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
585
611
 
612
+ // Agent-token login stores the identity only — never a default organization.
586
613
  saveCredentials({
587
614
  agent_token: agentToken,
588
- platform_url: platformUrl,
589
- ...(preselectedOrganizationId !== null ? { organization_id: preselectedOrganizationId } : {})
615
+ platform_url: platformUrl
590
616
  });
591
617
 
592
618
  console.log(`Agent token saved. The CLI will use this token for all protocol commands.`);
619
+ console.log(` Organizations are resolved from your token's membership per command.`);
593
620
  if (preselectedOrganizationId !== null) {
594
- console.log(` Default organization: ${preselectedOrganizationId}`);
595
- } else {
596
- console.log(` Organization will be derived from your token's membership.`);
621
+ console.log(
622
+ ` Note: --organization-id is no longer stored as a default; pass it per command (or use a ticket id) to scope an action.`
623
+ );
597
624
  }
598
625
  console.log(` Platform URL: ${platformUrl}`);
599
626
  console.log(`\nTo remove: ovld auth logout`);
600
627
  }
601
628
 
629
+ async function printOrganizationMembershipStatus() {
630
+ try {
631
+ const auth = await resolveAuth();
632
+ const organizations = await resolveOrganizations(auth);
633
+ if (!organizations.length) {
634
+ console.log(' Organizations: none (not a member of any organization)');
635
+ return;
636
+ }
637
+ console.log(` Organizations (${organizations.length}):`);
638
+ for (const organization of organizations) {
639
+ console.log(` - ${describeOrganization(organization)}`);
640
+ }
641
+ } catch (error) {
642
+ console.log(
643
+ ` Organizations: unavailable (${error instanceof Error ? error.message : String(error)})`
644
+ );
645
+ }
646
+ }
647
+
602
648
  async function printVerboseAuthStatus() {
603
649
  const status = await getAuthStatus();
604
650
  if (!status.isLoggedIn) {
@@ -617,7 +663,9 @@ async function printVerboseAuthStatus() {
617
663
  console.log(` Token source: ${status.tokenSource}`);
618
664
  console.log(` Token present: ${status.tokenPresent ? 'yes' : 'no'}`);
619
665
  console.log(` Auth mode: ${status.authMode}`);
620
- console.log(` Organization ID: ${status.organizationId ?? 'none'}`);
666
+ if (status.isLoggedIn) {
667
+ await printOrganizationMembershipStatus();
668
+ }
621
669
  if (status.error) {
622
670
  console.log(` Error: ${status.error}`);
623
671
  }
@@ -681,10 +729,11 @@ Subcommands:
681
729
  login Authorize the CLI via browser (works locally or over SSH)
682
730
  --token <oat_…> Persist an agent token from Settings → Agents & MCP.
683
731
  Skips the browser flow; token never expires.
684
- --organization-id <id> Optional default organization override
685
- (ticket-scoped commands infer organization
686
- from ticket ids such as 1:899)
687
- status Show current login status (use --verbose for redacted diagnostics)
732
+ --organization-id <id> Optional. Validated against your membership but
733
+ no longer stored as a default — the CLI is
734
+ organization-agnostic. Scope a command with a
735
+ ticket id (e.g. 1:899) or --organization-id.
736
+ status Show current login status (use --verbose to list your organizations)
688
737
  repair Mirror and chmod shared Desktop/CLI credentials when possible
689
738
  logout Remove stored credentials
690
739
  `);
@@ -584,12 +584,12 @@ export async function resolveAuth(options = {}) {
584
584
  typeof process.env.OVERLORD_ORGANIZATION_ID === 'string'
585
585
  ? Number.parseInt(process.env.OVERLORD_ORGANIZATION_ID, 10)
586
586
  : null;
587
- const resolvedOrganizationId = organizationIdHint ?? envOrganizationId;
588
- if (!Number.isFinite(resolvedOrganizationId)) {
589
- throw new Error(
590
- 'OVERLORD_ACCESS_TOKEN requires OVERLORD_ORGANIZATION_ID so protocol requests stay scoped.'
591
- );
592
- }
587
+ // Organization is no longer required up front. When neither a per-action hint
588
+ // nor an explicit OVERLORD_ORGANIZATION_ID override is present, the platform
589
+ // resolves the organization from the token owner's membership, so the CLI stays
590
+ // org-agnostic instead of pinning a single default.
591
+ const resolvedOrganizationId =
592
+ organizationIdHint ?? (Number.isFinite(envOrganizationId) ? envOrganizationId : null);
593
593
 
594
594
  if (isEnvAccessTokenUsable(envAccessToken)) {
595
595
  return {
@@ -603,11 +603,14 @@ export async function resolveAuth(options = {}) {
603
603
  }
604
604
 
605
605
  if (creds?.agent_token) {
606
+ // Stored credentials never act as a default organization. Org is taken from the
607
+ // per-action hint (ticket id or --organization-id) and otherwise left to the
608
+ // platform's membership lookup.
606
609
  return {
607
610
  platformUrl,
608
611
  bearerToken: creds.agent_token,
609
612
  localSecret,
610
- organizationId: organizationIdHint ?? creds.organization_id ?? null,
613
+ organizationId: organizationIdHint ?? null,
611
614
  authMode: 'agent_token'
612
615
  };
613
616
  }
@@ -641,19 +644,18 @@ export async function resolveAuth(options = {}) {
641
644
  }
642
645
  }
643
646
 
644
- if (!Number.isFinite(nextCredentials.organization_id)) {
645
- throw new Error('Overlord login is missing an organization selection. Run `ovld auth login` again.');
646
- }
647
-
648
647
  if (!nextCredentials.access_token) {
649
648
  throw new Error('No OAuth access token is available. Run `ovld auth login` again.');
650
649
  }
651
650
 
651
+ // OAuth sessions are not bound to a stored default organization. The platform
652
+ // derives the organization from the signed-in user's membership when no
653
+ // per-action hint is supplied, keeping the CLI org-agnostic.
652
654
  return {
653
655
  platformUrl,
654
656
  bearerToken: nextCredentials.access_token,
655
657
  localSecret,
656
- organizationId: organizationIdHint ?? nextCredentials.organization_id,
658
+ organizationId: organizationIdHint ?? null,
657
659
  authMode: 'oauth'
658
660
  };
659
661
  }
@@ -667,6 +669,122 @@ export async function resolveAuth(options = {}) {
667
669
  };
668
670
  }
669
671
 
672
+ /**
673
+ * Lists every organization the authenticated identity belongs to.
674
+ *
675
+ * This is the org-agnostic replacement for a stored "default organization": the
676
+ * caller's identity (OAuth session or agent token) resolves to the full set of
677
+ * memberships, and commands fan out or prompt over that list instead of assuming
678
+ * a single org. Works for OAuth, agent tokens, and local-dev alike because
679
+ * `/api/protocol/organizations` resolves the caller through the standard
680
+ * protocol auth path.
681
+ *
682
+ * @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
683
+ * @returns {Promise<Array<{ id: number, name: string }>>}
684
+ */
685
+ export async function resolveOrganizations(auth) {
686
+ const { platformUrl, bearerToken, localSecret } = auth;
687
+ let res;
688
+ try {
689
+ res = await fetch(`${platformUrl}/api/protocol/organizations`, {
690
+ method: 'POST',
691
+ headers: {
692
+ ...buildAuthHeaders(bearerToken, localSecret),
693
+ 'Content-Type': 'application/json'
694
+ },
695
+ body: JSON.stringify({})
696
+ });
697
+ } catch (error) {
698
+ throw describeNetworkError(error, `Failed to list organizations from ${platformUrl}`);
699
+ }
700
+
701
+ const data = await res.json().catch(() => ({}));
702
+ if (!res.ok) {
703
+ throw new Error(
704
+ `Failed to list organizations (${res.status}): ${data.error ?? JSON.stringify(data)}`
705
+ );
706
+ }
707
+
708
+ return Array.isArray(data.organizations)
709
+ ? data.organizations
710
+ .map(org => ({ id: org.id, name: typeof org.name === 'string' ? org.name : '' }))
711
+ .filter(org => Number.isFinite(org.id))
712
+ : [];
713
+ }
714
+
715
+ /**
716
+ * Searches tickets across every organization the identity belongs to.
717
+ *
718
+ * Organization-agnostic by default: with no explicit organization it resolves the
719
+ * membership list and fans out the per-org search, merging and de-duplicating by
720
+ * ticket id. An explicit `organizationId` (from --organization-id) scopes the
721
+ * search to that single org instead. There is no default-org fallback.
722
+ *
723
+ * @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
724
+ * @param {Record<string, unknown>} body search-tickets request body
725
+ * @param {{ organizationId?: number | null }} [options]
726
+ * @returns {Promise<{ tickets: unknown[], count: number }>}
727
+ */
728
+ export async function searchTicketsAcrossOrganizations(auth, body, options = {}) {
729
+ const { platformUrl, bearerToken, localSecret } = auth;
730
+ const explicitOrganizationId =
731
+ Number.isFinite(options.organizationId) ? options.organizationId : null;
732
+
733
+ const post = async organizationId => {
734
+ const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
735
+ method: 'POST',
736
+ headers: {
737
+ ...buildAuthHeaders(bearerToken, localSecret, organizationId),
738
+ 'Content-Type': 'application/json'
739
+ },
740
+ body: JSON.stringify(body)
741
+ });
742
+ const data = await res.json().catch(() => ({}));
743
+ if (!res.ok) {
744
+ throw new Error(
745
+ `Failed to search tickets (${res.status}): ${data.error ?? JSON.stringify(data)}`
746
+ );
747
+ }
748
+ return Array.isArray(data.tickets) ? data.tickets : [];
749
+ };
750
+
751
+ if (explicitOrganizationId !== null) {
752
+ const tickets = await post(explicitOrganizationId);
753
+ return { tickets, count: tickets.length };
754
+ }
755
+
756
+ const organizations = await resolveOrganizations(auth);
757
+ if (!organizations.length) {
758
+ const tickets = await post(null);
759
+ return { tickets, count: tickets.length };
760
+ }
761
+
762
+ const results = await Promise.allSettled(organizations.map(org => post(org.id)));
763
+ const failures = results
764
+ .map((result, index) => ({ result, organization: organizations[index] }))
765
+ .filter(({ result }) => result.status === 'rejected');
766
+ if (failures.length > 0) {
767
+ const detail = failures
768
+ .map(({ result, organization }) => {
769
+ const reason = result.status === 'rejected' ? result.reason : null;
770
+ const message = reason instanceof Error ? reason.message : String(reason);
771
+ return `${organization.name || `organization ${organization.id}`} (${organization.id}): ${message}`;
772
+ })
773
+ .join('; ');
774
+ throw new Error(`Ticket search returned partial failures: ${detail}`);
775
+ }
776
+
777
+ const perOrg = results.map(result => (result.status === 'fulfilled' ? result.value : []));
778
+ const byId = new Map();
779
+ for (const ticket of perOrg.flat()) {
780
+ if (ticket && ticket.id && !byId.has(ticket.id)) {
781
+ byId.set(ticket.id, ticket);
782
+ }
783
+ }
784
+ const tickets = [...byId.values()];
785
+ return { tickets, count: tickets.length };
786
+ }
787
+
670
788
  export async function getAuthStatus() {
671
789
  const creds = loadCredentials();
672
790
  let resolved;
@@ -17,6 +17,21 @@ import { runAttachCommand } from './attach.mjs';
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
18
  const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
19
19
  const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
20
+ const CLAUDE_PLUGIN_CACHE_DIR = path.join(
21
+ os.homedir(),
22
+ '.claude',
23
+ 'plugins',
24
+ 'cache',
25
+ 'overlord-local',
26
+ 'overlord'
27
+ );
28
+ const CLAUDE_INSTALLED_PLUGINS_FILE = path.join(
29
+ os.homedir(),
30
+ '.claude',
31
+ 'plugins',
32
+ 'installed_plugins.json'
33
+ );
34
+ const CLAUDE_INSTALLED_PLUGIN_KEY = 'overlord@overlord-local';
20
35
 
21
36
  function resolveLaunchScratchDir(workingDirectory, { explicit = false } = {}) {
22
37
  const trimmed = typeof workingDirectory === 'string' ? workingDirectory.trim() : '';
@@ -47,6 +62,55 @@ function claudeSourcePluginDir() {
47
62
  return null;
48
63
  }
49
64
 
65
+ function readJsonFile(filePath) {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function claudeInstalledPluginDir() {
74
+ const installed = readJsonFile(CLAUDE_INSTALLED_PLUGINS_FILE);
75
+ const pluginEntries = installed?.plugins?.[CLAUDE_INSTALLED_PLUGIN_KEY];
76
+
77
+ if (Array.isArray(pluginEntries)) {
78
+ for (const entry of pluginEntries) {
79
+ const installPath = typeof entry?.installPath === 'string' ? entry.installPath.trim() : '';
80
+ if (installPath && fs.existsSync(path.join(installPath, '.claude-plugin', 'plugin.json'))) {
81
+ return installPath;
82
+ }
83
+ }
84
+ }
85
+
86
+ if (!fs.existsSync(CLAUDE_PLUGIN_CACHE_DIR)) return null;
87
+
88
+ const versionDirs = fs
89
+ .readdirSync(CLAUDE_PLUGIN_CACHE_DIR, { withFileTypes: true })
90
+ .filter(entry => entry.isDirectory())
91
+ .map(entry => path.join(CLAUDE_PLUGIN_CACHE_DIR, entry.name))
92
+ .filter(candidate => fs.existsSync(path.join(candidate, '.claude-plugin', 'plugin.json')))
93
+ .sort()
94
+ .reverse();
95
+
96
+ return versionDirs[0] ?? null;
97
+ }
98
+
99
+ function envTruthy(value) {
100
+ return /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
101
+ }
102
+
103
+ function claudeLaunchPluginDir() {
104
+ const explicitPluginDir = process.env.OVERLORD_CLAUDE_PLUGIN_DIR?.trim();
105
+ if (explicitPluginDir) return explicitPluginDir;
106
+
107
+ if (envTruthy(process.env.OVERLORD_USE_CLAUDE_SOURCE_PLUGIN)) {
108
+ return claudeSourcePluginDir();
109
+ }
110
+
111
+ return null;
112
+ }
113
+
50
114
  function antigravityBundleInstalled() {
51
115
  return fs.existsSync(
52
116
  path.join(os.homedir(), '.gemini', 'antigravity-cli', 'plugins', 'plugin.json')
@@ -55,7 +119,7 @@ function antigravityBundleInstalled() {
55
119
 
56
120
  function getInstructionMode(agent) {
57
121
  if (agent === 'claude') {
58
- return claudeSourcePluginDir() ? 'bundle' : 'legacy';
122
+ return claudeInstalledPluginDir() || claudeLaunchPluginDir() ? 'bundle' : 'legacy';
59
123
  }
60
124
 
61
125
  if (agent === 'codex') {
@@ -474,7 +538,7 @@ async function runAgent(agent, mode = 'run', options = {}) {
474
538
 
475
539
  try {
476
540
  if (agent === 'claude') {
477
- const pluginDir = claudeSourcePluginDir();
541
+ const pluginDir = claudeLaunchPluginDir();
478
542
  if (mode === 'resume') {
479
543
  const claudeSessionId = process.env.CLAUDE_SESSION_ID?.trim();
480
544
  const args = claudeSessionId
@@ -4,7 +4,7 @@ import readline from 'node:readline';
4
4
  import { stdin as input, stdout as output } from 'node:process';
5
5
  import fs from 'node:fs';
6
6
 
7
- import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
7
+ import { buildAuthHeaders, resolveAuth, resolveOrganizations } from './credentials.mjs';
8
8
  import { runLauncherCommand } from './launcher.mjs';
9
9
 
10
10
  const PROMPT_AGENT_IDENTIFIERS = {
@@ -232,7 +232,57 @@ async function fetchProjects(platformUrl, bearerToken, localSecret, organization
232
232
  );
233
233
  }
234
234
 
235
- return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
235
+ return Array.isArray(data.projects) ? data.projects : [];
236
+ }
237
+
238
+ /**
239
+ * Lists projects across every organization the identity belongs to.
240
+ *
241
+ * The CLI is organization-agnostic: instead of listing only a single default
242
+ * org's projects, it resolves the membership list and fans out the per-org
243
+ * projects query, then merges and de-duplicates by project id. Each project
244
+ * carries its own `organizationId`, so the caller scopes follow-up writes (ticket
245
+ * creation, resource registration) to the chosen project's org — no default org.
246
+ *
247
+ * @param {{ platformUrl: string, bearerToken: string, localSecret?: string }} auth
248
+ */
249
+ export async function fetchProjectsAcrossOrganizations(auth) {
250
+ const { platformUrl, bearerToken, localSecret } = auth;
251
+ const organizations = await resolveOrganizations(auth);
252
+
253
+ // No memberships resolved (or a backend that scopes by membership anyway): fall
254
+ // back to a single unscoped query so the command still works.
255
+ if (!organizations.length) {
256
+ return sortProjects(await fetchProjects(platformUrl, bearerToken, localSecret, null));
257
+ }
258
+
259
+ const results = await Promise.allSettled(
260
+ organizations.map(org => fetchProjects(platformUrl, bearerToken, localSecret, org.id))
261
+ );
262
+ const failures = results
263
+ .map((result, index) => ({ result, organization: organizations[index] }))
264
+ .filter(({ result }) => result.status === 'rejected');
265
+ if (failures.length > 0) {
266
+ const detail = failures
267
+ .map(({ result, organization }) => {
268
+ const reason = result.status === 'rejected' ? result.reason : null;
269
+ const message = reason instanceof Error ? reason.message : String(reason);
270
+ return `${organization.name || `organization ${organization.id}`} (${organization.id}): ${message}`;
271
+ })
272
+ .join('; ');
273
+ throw new Error(`Project listing returned partial failures: ${detail}`);
274
+ }
275
+
276
+ const perOrg = results.map(result => (result.status === 'fulfilled' ? result.value : []));
277
+
278
+ const byId = new Map();
279
+ for (const project of perOrg.flat()) {
280
+ if (project && project.id && !byId.has(project.id)) {
281
+ byId.set(project.id, project);
282
+ }
283
+ }
284
+
285
+ return sortProjects([...byId.values()]);
236
286
  }
237
287
 
238
288
  async function createTicket(platformUrl, bearerToken, localSecret, organizationId, body) {
@@ -312,8 +362,9 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
312
362
  const { flags } = parseFlags(args);
313
363
  const objectiveInput = await resolveObjectiveInput(flags);
314
364
 
315
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
316
- const projects = await fetchProjects(platformUrl, bearerToken, localSecret, organizationId);
365
+ const auth = await resolveAuth();
366
+ const { platformUrl, bearerToken, localSecret } = auth;
367
+ const projects = await fetchProjectsAcrossOrganizations(auth);
317
368
 
318
369
  if (!projects.length) {
319
370
  throw new Error('No projects available. Create a project first.');
@@ -328,6 +379,10 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
328
379
  renderItem: project => projectLabel(project)
329
380
  }));
330
381
 
382
+ // The ticket is created in the organization that owns the chosen project,
383
+ // never a stored default org.
384
+ const organizationId = selectedProject.organizationId ?? null;
385
+
331
386
  const selectedAgent = launchAgent
332
387
  ? (resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
333
388
  (await promptForSelection({
@@ -5,7 +5,12 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
 
8
- import { buildAuthHeaders, getAuthStatus, resolveAuth } from './credentials.mjs';
8
+ import {
9
+ buildAuthHeaders,
10
+ getAuthStatus,
11
+ resolveAuth,
12
+ searchTicketsAcrossOrganizations
13
+ } from './credentials.mjs';
9
14
 
10
15
  /**
11
16
  * Parse simple CLI flags: --key value or --key=value
@@ -2619,9 +2624,7 @@ async function protocolAddObjectives(args) {
2619
2624
 
2620
2625
  async function protocolSearchTickets(args) {
2621
2626
  const flags = parseFlags(args);
2622
- const { platformUrl, bearerToken, localSecret, organizationId } =
2623
- await resolveProtocolAuthForFlags(flags);
2624
- const timeoutMs = resolveTimeout(flags);
2627
+ const auth = await resolveProtocolAuthForFlags(flags);
2625
2628
 
2626
2629
  const statuses = flags.status
2627
2630
  ? String(flags.status)
@@ -2646,15 +2649,11 @@ async function protocolSearchTickets(args) {
2646
2649
  ...(flags['updated-before'] ? { updatedBefore: String(flags['updated-before']) } : {})
2647
2650
  };
2648
2651
 
2649
- const data = await apiPost(
2650
- platformUrl,
2651
- bearerToken,
2652
- localSecret,
2653
- organizationId,
2654
- '/api/protocol/search-tickets',
2655
- body,
2656
- timeoutMs
2657
- );
2652
+ // Organization-agnostic: with no --organization-id this fans out across every
2653
+ // organization you belong to and merges the results; an explicit org scopes it.
2654
+ const data = await searchTicketsAcrossOrganizations(auth, body, {
2655
+ organizationId: auth.organizationId ?? null
2656
+ });
2658
2657
  console.log(JSON.stringify(data, null, 2));
2659
2658
  }
2660
2659
 
@@ -1138,7 +1138,9 @@ function installClaude() {
1138
1138
  } else {
1139
1139
  console.log(' ✓ No v3.25 Claude connector files needed migration.');
1140
1140
  }
1141
- console.log(' ✓ `ovld launch claude` now loads this plugin with `claude --plugin-dir`.');
1141
+ console.log(
1142
+ ' ✓ `ovld launch claude` now uses the installed Claude local marketplace plugin by default.'
1143
+ );
1142
1144
 
1143
1145
  const manifest = readManifest();
1144
1146
  manifest.claude = {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
3
+ import { resolveAuth, searchTicketsAcrossOrganizations } from './credentials.mjs';
4
4
  import { runCreateCommand } from './new-ticket.mjs';
5
5
 
6
6
  /**
@@ -33,23 +33,10 @@ function parseFlags(args, knownFlags) {
33
33
  return result;
34
34
  }
35
35
 
36
- async function apiPost(url, token, localSecret, organizationId, body) {
37
- const res = await fetch(url, {
38
- method: 'POST',
39
- headers: {
40
- ...buildAuthHeaders(token, localSecret, organizationId),
41
- 'Content-Type': 'application/json'
42
- },
43
- body: JSON.stringify(body)
44
- });
45
-
46
- const data = await res.json().catch(() => ({}));
47
-
48
- if (!res.ok) {
49
- throw new Error(`API error (${res.status}): ${data.error ?? JSON.stringify(data)}`);
50
- }
51
-
52
- return data;
36
+ function parseOrganizationId(value) {
37
+ if (typeof value !== 'string' && typeof value !== 'number') return null;
38
+ const parsed = Number.parseInt(String(value), 10);
39
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
53
40
  }
54
41
 
55
42
  export async function ticketsCreate(args) {
@@ -57,22 +44,20 @@ export async function ticketsCreate(args) {
57
44
  }
58
45
 
59
46
  export async function ticketsList(args) {
60
- const flags = parseFlags(args, ['status', 'include-completed']);
47
+ const flags = parseFlags(args, ['status', 'include-completed', 'organization-id']);
61
48
 
62
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
49
+ const auth = await resolveAuth();
63
50
 
64
51
  const body = {
65
52
  includeCompleted: flags['include-completed'] !== false,
66
53
  ...(flags.status ? { statuses: [String(flags.status)] } : {})
67
54
  };
68
55
 
69
- const data = await apiPost(
70
- `${platformUrl}/api/protocol/search-tickets`,
71
- bearerToken,
72
- localSecret,
73
- organizationId,
74
- body
75
- );
56
+ // Organization-agnostic: searches every organization you belong to unless
57
+ // --organization-id scopes it to one.
58
+ const data = await searchTicketsAcrossOrganizations(auth, body, {
59
+ organizationId: parseOrganizationId(flags['organization-id'])
60
+ });
76
61
 
77
62
  if (!data.tickets?.length) {
78
63
  console.log('No tickets found.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2606021051.0",
3
+ "version": "0.2606041130.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",
@@ -272,8 +272,8 @@ This keeps the ticket feed readable while preserving the full document in versio
272
272
  - When a summary or question contains backticks, `$vars`, or other shell-special characters, always use `--summary-file -` (or `--question-file -`) with a single-quoted heredoc (`<<'EOF'`). Never retry by stripping or escaping content — pipe stdin instead.
273
273
  - Use `write-context` for facts a future agent session should know.
274
274
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
275
- - If you must run `ovld auth login`, `--organization-id <id>` is optional — use it only when you want to set a different default organization than the CLI would choose automatically.
275
+ - If you must run `ovld auth login`, `--organization-id <id>` is optional — it validates/scopes that login or command but does not create a stored default organization.
276
276
  - Do not add or commit changes unless the user explicitly asks you to commit.
277
277
  - Delivery is the concluding step. After delivering, stop implementation work unless the user explicitly asks for follow-up execution; once follow-up execution is complete, deliver again.
278
278
 
279
- <!-- version: 0.1.6 -->
279
+ <!-- version: 0.1.8 -->
@@ -102,7 +102,7 @@ For the `record-change-rationales` command and full payload shape with optional
102
102
  - When a summary or question contains backticks, `$vars`, or other shell-special characters, always use `--summary-file -` (or `--question-file -`) with a single-quoted heredoc (`<<'EOF'`). Never retry by stripping or escaping content — pipe stdin instead. See [reference/shell-escaping.md](reference/shell-escaping.md).
103
103
  - Use `write-context` for facts a future agent session should know.
104
104
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
105
- - If you must run `ovld auth login`, `--organization-id <id>` is optional — use it only when you want to set a different default organization than the CLI would choose automatically.
105
+ - If you must run `ovld auth login`, `--organization-id <id>` is optional — it validates/scopes that login or command but does not create a stored default organization.
106
106
  - Do not add or commit changes unless the user explicitly asks you to commit.
107
107
  - Delivery is the concluding step. After delivering, stop implementation work unless the user explicitly asks for follow-up execution; once follow-up execution is complete, deliver again.
108
108
 
@@ -114,5 +114,5 @@ For the `record-change-rationales` command and full payload shape with optional
114
114
  - [reference/context.md](reference/context.md) — Shared state, attachments, and large artifact policy
115
115
  - [reference/shell-escaping.md](reference/shell-escaping.md) — Heredoc stdin piping for special characters in summaries and payloads
116
116
 
117
- <!-- version: 0.5.10 -->
117
+ <!-- version: 0.5.11 -->
118
118
 
@@ -102,7 +102,7 @@ For the `record-change-rationales` command and full payload shape with optional
102
102
  - When a summary or question contains backticks, `$vars`, or other shell-special characters, always use `--summary-file -` (or `--question-file -`) with a single-quoted heredoc (`<<'EOF'`). Never retry by stripping or escaping content — pipe stdin instead. See [reference/shell-escaping.md](reference/shell-escaping.md).
103
103
  - Use `write-context` for facts a future agent session should know.
104
104
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
105
- - If you must run `ovld auth login`, `--organization-id <id>` is optional — use it only when you want to set a different default organization than the CLI would choose automatically.
105
+ - If you must run `ovld auth login`, `--organization-id <id>` is optional — it validates/scopes that login or command but does not create a stored default organization.
106
106
  - Do not add or commit changes unless the user explicitly asks you to commit.
107
107
  - Delivery is the concluding step. After delivering, stop implementation work unless the user explicitly asks for follow-up execution; once follow-up execution is complete, deliver again.
108
108
 
@@ -114,5 +114,5 @@ For the `record-change-rationales` command and full payload shape with optional
114
114
  - [reference/context.md](reference/context.md) — Shared state, attachments, and large artifact policy
115
115
  - [reference/shell-escaping.md](reference/shell-escaping.md) — Heredoc stdin piping for special characters in summaries and payloads
116
116
 
117
- <!-- version: 0.5.10 -->
117
+ <!-- version: 0.5.11 -->
118
118
 
@@ -102,7 +102,7 @@ For the `record-change-rationales` command and full payload shape with optional
102
102
  - When a summary or question contains backticks, `$vars`, or other shell-special characters, always use `--summary-file -` (or `--question-file -`) with a single-quoted heredoc (`<<'EOF'`). Never retry by stripping or escaping content — pipe stdin instead. See [reference/shell-escaping.md](reference/shell-escaping.md).
103
103
  - Use `write-context` for facts a future agent session should know.
104
104
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
105
- - If you must run `ovld auth login`, `--organization-id <id>` is optional — use it only when you want to set a different default organization than the CLI would choose automatically.
105
+ - If you must run `ovld auth login`, `--organization-id <id>` is optional — it validates/scopes that login or command but does not create a stored default organization.
106
106
  - Do not add or commit changes unless the user explicitly asks you to commit.
107
107
  - Do not create or rely on a local Codex `AGENTS.md` bundle for Overlord.
108
108
  - Delivery is the concluding step. After delivering, stop implementation work unless the user explicitly asks for follow-up execution; once follow-up execution is complete, deliver again.
@@ -115,5 +115,5 @@ For the `record-change-rationales` command and full payload shape with optional
115
115
  - [reference/context.md](reference/context.md) — Shared state, attachments, and large artifact policy
116
116
  - [reference/shell-escaping.md](reference/shell-escaping.md) — Heredoc stdin piping for special characters in summaries and payloads
117
117
 
118
- <!-- version: 0.5.10 -->
118
+ <!-- version: 0.5.11 -->
119
119