@postplus/cli 0.1.19 → 0.1.21

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.
@@ -1,7 +1,8 @@
1
1
  import { refreshRemoteAuthSession } from './auth-session.js';
2
2
  import { clearAuthState, generateAuthStatusReport } from './auth.js';
3
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusClientUpgradeError, } from './client-compatibility.js';
3
4
  import { requireHostedBaseUrl } from './hosted-release.js';
4
- import { resolveAccessTokenState, resolveRefreshTokenState, } from './local-state.js';
5
+ import { resolveCliSessionTokenState } from './local-state.js';
5
6
  export async function refreshRemoteAuth() {
6
7
  const refreshed = await refreshRemoteAuthSession();
7
8
  return {
@@ -25,31 +26,31 @@ export function formatAuthRefreshReport(report) {
25
26
  ].join('\n');
26
27
  }
27
28
  export async function revokeRemoteAuth() {
28
- const [apiBaseUrl, accessTokenState, refreshTokenState] = await Promise.all([
29
+ const [apiBaseUrl, cliSessionTokenState] = await Promise.all([
29
30
  requireHostedBaseUrl(),
30
- resolveAccessTokenState(),
31
- resolveRefreshTokenState(),
31
+ resolveCliSessionTokenState(),
32
32
  ]);
33
- if (!accessTokenState.present || !accessTokenState.value) {
34
- throw new Error('Run `postplus auth login` before revoking PostPlus auth.');
35
- }
36
- if (!refreshTokenState.present || !refreshTokenState.value) {
33
+ if (!cliSessionTokenState.present || !cliSessionTokenState.value) {
37
34
  throw new Error('Run `postplus auth login` before revoking PostPlus auth.');
38
35
  }
36
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
39
37
  const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/revoke`, {
40
38
  method: 'POST',
41
39
  headers: {
42
40
  accept: 'application/json',
43
- authorization: `Bearer ${accessTokenState.value}`,
41
+ ...compatibilityHeaders,
42
+ authorization: `Bearer ${cliSessionTokenState.value}`,
44
43
  'content-type': 'application/json',
45
44
  },
46
- body: JSON.stringify({
47
- refreshToken: refreshTokenState.value,
48
- }),
45
+ body: JSON.stringify({}),
49
46
  signal: AbortSignal.timeout(15000),
50
47
  });
51
48
  const payload = (await response.json());
52
49
  if (!response.ok) {
50
+ if ('code' in payload &&
51
+ payload.code === 'postplus_client_upgrade_required') {
52
+ throw new Error(formatPostPlusClientUpgradeError(payload));
53
+ }
53
54
  throw new Error('error' in payload && typeof payload.error === 'string'
54
55
  ? payload.error
55
56
  : 'Failed to revoke remote PostPlus auth.');
@@ -1,3 +1,4 @@
1
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusClientUpgradeError, writeCurrentCliVersionToLocalConfig, } from './client-compatibility.js';
1
2
  import { requireHostedBaseUrl } from './hosted-release.js';
2
3
  import { setLocalSession } from './local-state.js';
3
4
  export const CLI_AUTH_LOGIN_TIMEOUT_MS = 30 * 60 * 1000;
@@ -23,18 +24,18 @@ export async function loginWithCloudHandoff() {
23
24
  requestId: started.requestId,
24
25
  });
25
26
  const validated = await validateCliSession({
26
- accessToken: handoffPayload.accessToken,
27
27
  apiBaseUrl: baseUrl,
28
+ cliSessionToken: handoffPayload.cliSessionToken,
28
29
  });
29
30
  await setLocalSession({
30
- accessToken: handoffPayload.accessToken,
31
31
  accountId: validated.accountId,
32
32
  apiBaseUrl: baseUrl,
33
- refreshToken: handoffPayload.refreshToken,
33
+ cliSessionToken: handoffPayload.cliSessionToken,
34
34
  sessionExpiresAt: validated.sessionExpiresAt ?? handoffPayload.sessionExpiresAt ?? null,
35
35
  userEmail: validated.userEmail,
36
36
  userId: validated.userId,
37
37
  });
38
+ await writeCurrentCliVersionToLocalConfig();
38
39
  return {
39
40
  accountId: validated.accountId,
40
41
  apiBaseUrl: baseUrl,
@@ -44,10 +45,12 @@ export async function loginWithCloudHandoff() {
44
45
  };
45
46
  }
46
47
  export async function startCloudAuthLogin(apiBaseUrl) {
48
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
47
49
  const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/login/start`, {
48
50
  method: 'POST',
49
51
  headers: {
50
52
  accept: 'application/json',
53
+ ...compatibilityHeaders,
51
54
  },
52
55
  signal: AbortSignal.timeout(15000),
53
56
  });
@@ -76,10 +79,12 @@ async function waitForCloudAuthLogin(input) {
76
79
  throw new Error('Timed out waiting for the cloud sign-in handoff.');
77
80
  }
78
81
  export async function pollCloudAuthLogin(input) {
82
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
79
83
  const response = await fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/login/poll`, {
80
84
  method: 'POST',
81
85
  headers: {
82
86
  accept: 'application/json',
87
+ ...compatibilityHeaders,
83
88
  'content-type': 'application/json',
84
89
  },
85
90
  body: JSON.stringify({
@@ -101,11 +106,13 @@ export async function pollCloudAuthLogin(input) {
101
106
  throw new Error('PostPlus CLI sign-in poll returned incomplete data.');
102
107
  }
103
108
  export async function validateCliSession(input) {
109
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
104
110
  const response = await fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
105
111
  method: 'GET',
106
112
  headers: {
107
113
  accept: 'application/json',
108
- authorization: `Bearer ${input.accessToken}`,
114
+ ...compatibilityHeaders,
115
+ authorization: `Bearer ${input.cliSessionToken}`,
109
116
  },
110
117
  signal: AbortSignal.timeout(15000),
111
118
  });
@@ -123,6 +130,9 @@ export function formatCliSessionAuthError(payload) {
123
130
  'Once the environment is ready, the CLI will automatically obtain and store its session.',
124
131
  ].join(' ');
125
132
  }
133
+ if (payload.code === 'postplus_client_upgrade_required') {
134
+ return formatPostPlusClientUpgradeError(payload);
135
+ }
126
136
  if (typeof payload.error === 'string' && payload.error.trim().length > 0) {
127
137
  return payload.error;
128
138
  }
@@ -140,8 +150,7 @@ function isCliAuthLoginStartSuccessPayload(payload) {
140
150
  function isCliAuthLoginCompletedPayload(payload) {
141
151
  return ('status' in payload &&
142
152
  payload.status === 'completed' &&
143
- typeof payload.accessToken === 'string' &&
144
- typeof payload.refreshToken === 'string' &&
153
+ typeof payload.cliSessionToken === 'string' &&
145
154
  typeof payload.accountId === 'string' &&
146
155
  typeof payload.userId === 'string');
147
156
  }
@@ -149,6 +158,9 @@ function isCliAuthLoginPendingPayload(payload) {
149
158
  return 'status' in payload && payload.status === 'pending';
150
159
  }
151
160
  function formatRemoteAuthLoginError(payload) {
161
+ if ('code' in payload && payload.code === 'postplus_client_upgrade_required') {
162
+ return formatPostPlusClientUpgradeError(payload);
163
+ }
152
164
  return 'error' in payload &&
153
165
  typeof payload.error === 'string' &&
154
166
  payload.error.trim().length > 0
@@ -1,137 +1,89 @@
1
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusClientUpgradeError, writeCurrentCliVersionToLocalConfig, } from './client-compatibility.js';
1
2
  import { requireHostedBaseUrl } from './hosted-release.js';
2
- import { readLocalConfig, resolveAccessTokenState, resolveRefreshTokenState, setLocalSession, } from './local-state.js';
3
- export const AUTH_SESSION_REFRESH_LEEWAY_SECONDS = 60;
3
+ import { resolveCliSessionTokenState, setLocalSession, } from './local-state.js';
4
4
  export async function resolveFreshRemoteAuth(options = {}) {
5
- const [apiBaseUrl, accessTokenState, refreshTokenState, config] = await Promise.all([
5
+ const [apiBaseUrl, cliSessionTokenState] = await Promise.all([
6
6
  requireHostedBaseUrl(),
7
- resolveAccessTokenState(),
8
- resolveRefreshTokenState(),
9
- readLocalConfig(),
7
+ resolveCliSessionTokenState(),
10
8
  ]);
11
- if (!refreshTokenState.present || !refreshTokenState.value) {
12
- if (!accessTokenState.present || !accessTokenState.value) {
13
- throw new Error('Run `postplus auth login` before validating PostPlus auth.');
14
- }
15
- return {
16
- accessToken: accessTokenState.value,
17
- apiBaseUrl,
18
- refreshed: false,
19
- source: 'config',
20
- };
9
+ if (!cliSessionTokenState.present || !cliSessionTokenState.value) {
10
+ throw new Error('Run `postplus auth login` before using PostPlus auth.');
21
11
  }
22
- const existingAccessToken = accessTokenState.value;
23
- const decodedTokenExpiresAt = existingAccessToken
24
- ? decodeAccessTokenExpiration(existingAccessToken)
25
- : null;
26
- const tokenExpiresAt = typeof decodedTokenExpiresAt === 'number'
27
- ? decodedTokenExpiresAt
28
- : typeof config?.sessionExpiresAt === 'number'
29
- ? config.sessionExpiresAt
30
- : null;
31
- const shouldRefresh = options.forceRefresh === true ||
32
- !accessTokenState.present ||
33
- !accessTokenState.value ||
34
- isExpiringSoon(tokenExpiresAt);
35
- if (!shouldRefresh) {
36
- if (!existingAccessToken) {
37
- throw new Error('Run `postplus auth login` before validating PostPlus auth.');
38
- }
12
+ if (options.forceRefresh === true) {
13
+ const refreshed = await refreshRemoteAuthSession({
14
+ apiBaseUrl,
15
+ cliSessionToken: cliSessionTokenState.value,
16
+ });
39
17
  return {
40
- accessToken: existingAccessToken,
41
18
  apiBaseUrl,
42
- refreshed: false,
19
+ cliSessionToken: refreshed.cliSessionToken,
20
+ refreshed: true,
43
21
  source: 'config',
44
22
  };
45
23
  }
46
- const refreshed = await refreshRemoteAuthSession({
47
- accessToken: accessTokenState.value,
48
- apiBaseUrl,
49
- refreshToken: refreshTokenState.value,
50
- });
51
24
  return {
52
- accessToken: refreshed.accessToken,
53
25
  apiBaseUrl,
54
- refreshed: true,
26
+ cliSessionToken: cliSessionTokenState.value,
27
+ refreshed: false,
55
28
  source: 'config',
56
29
  };
57
30
  }
58
31
  export async function refreshRemoteAuthSession(input) {
59
- const [apiBaseUrl, accessTokenState, refreshTokenState] = await Promise.all([
32
+ const [apiBaseUrl, cliSessionTokenState] = await Promise.all([
60
33
  input?.apiBaseUrl ?? requireHostedBaseUrl(),
61
- input?.accessToken === undefined ? resolveAccessTokenState() : null,
62
- input?.refreshToken === undefined ? resolveRefreshTokenState() : null,
34
+ input?.cliSessionToken === undefined ? resolveCliSessionTokenState() : null,
63
35
  ]);
64
- const accessToken = input?.accessToken === undefined
65
- ? accessTokenState?.value
66
- : input.accessToken;
67
- const refreshToken = input?.refreshToken === undefined
68
- ? refreshTokenState?.value
69
- : input.refreshToken;
70
- if (!refreshToken) {
36
+ const cliSessionToken = input?.cliSessionToken === undefined
37
+ ? cliSessionTokenState?.value
38
+ : input.cliSessionToken;
39
+ if (!cliSessionToken) {
71
40
  throw new Error('Run `postplus auth login` before refreshing PostPlus auth.');
72
41
  }
42
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
73
43
  const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/refresh`, {
74
44
  method: 'POST',
75
45
  headers: {
76
46
  accept: 'application/json',
77
- ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
47
+ ...compatibilityHeaders,
48
+ authorization: `Bearer ${cliSessionToken}`,
78
49
  'content-type': 'application/json',
79
50
  },
80
- body: JSON.stringify({
81
- refreshToken,
82
- }),
51
+ body: JSON.stringify({}),
83
52
  signal: AbortSignal.timeout(15000),
84
53
  });
85
54
  const payload = (await response.json());
86
55
  if (!response.ok) {
56
+ if ('code' in payload &&
57
+ payload.code === 'postplus_client_upgrade_required') {
58
+ throw new Error(formatPostPlusClientUpgradeError(payload));
59
+ }
87
60
  throw new Error('error' in payload && typeof payload.error === 'string'
88
61
  ? payload.error
89
62
  : 'Failed to refresh remote PostPlus auth.');
90
63
  }
91
64
  if (!isRemoteAuthRefreshSuccessPayload(payload)) {
92
- throw new Error('PostPlus auth refresh returned incomplete session tokens.');
65
+ throw new Error('PostPlus auth refresh returned incomplete session data.');
93
66
  }
94
67
  await setLocalSession({
95
- accessToken: payload.accessToken,
96
68
  accountId: payload.accountId,
97
69
  apiBaseUrl,
98
- refreshToken: payload.refreshToken,
70
+ cliSessionToken: payload.cliSessionToken,
99
71
  sessionExpiresAt: payload.sessionExpiresAt,
100
72
  userEmail: payload.userEmail,
101
73
  userId: payload.userId,
102
74
  });
75
+ await writeCurrentCliVersionToLocalConfig();
103
76
  return {
104
77
  ...payload,
105
78
  apiBaseUrl,
106
79
  };
107
80
  }
108
- export function decodeAccessTokenExpiration(accessToken) {
109
- try {
110
- const [, payload] = accessToken.split('.');
111
- if (!payload) {
112
- return null;
113
- }
114
- const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
115
- return typeof decoded.exp === 'number' ? decoded.exp : null;
116
- }
117
- catch {
118
- return null;
119
- }
120
- }
121
- function isExpiringSoon(expiresAt) {
122
- if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) {
123
- return false;
124
- }
125
- const nowSeconds = Math.floor(Date.now() / 1_000);
126
- return expiresAt - nowSeconds <= AUTH_SESSION_REFRESH_LEEWAY_SECONDS;
127
- }
128
81
  function isRemoteAuthRefreshSuccessPayload(payload) {
129
82
  return (typeof payload === 'object' &&
130
83
  payload !== null &&
131
- typeof payload.accessToken === 'string' &&
132
- payload.accessToken.trim().length > 0 &&
133
- typeof payload.refreshToken === 'string' &&
134
- payload.refreshToken.trim().length > 0 &&
84
+ typeof payload.cliSessionToken ===
85
+ 'string' &&
86
+ payload.cliSessionToken.trim().length > 0 &&
135
87
  typeof payload.accountId === 'string' &&
136
88
  typeof payload.userId === 'string');
137
89
  }
@@ -1,4 +1,5 @@
1
1
  import { resolveFreshRemoteAuth } from './auth-session.js';
2
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusClientUpgradeError, } from './client-compatibility.js';
2
3
  export async function validateRemoteAuth() {
3
4
  let auth = await resolveFreshRemoteAuth();
4
5
  let response = await fetchWhoami(auth);
@@ -10,6 +11,10 @@ export async function validateRemoteAuth() {
10
11
  }
11
12
  const payload = (await response.json());
12
13
  if (!response.ok) {
14
+ if ('code' in payload &&
15
+ payload.code === 'postplus_client_upgrade_required') {
16
+ throw new Error(formatPostPlusClientUpgradeError(payload));
17
+ }
13
18
  throw new Error('error' in payload && typeof payload.error === 'string'
14
19
  ? payload.error
15
20
  : 'Failed to validate remote PostPlus auth.');
@@ -36,12 +41,14 @@ export function formatAuthValidateReport(report) {
36
41
  `Subscription: ${report.subscriptionStatus ?? 'unknown'}`,
37
42
  ].join('\n');
38
43
  }
39
- function fetchWhoami(input) {
44
+ async function fetchWhoami(input) {
45
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
40
46
  return fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
41
47
  method: 'GET',
42
48
  headers: {
43
49
  accept: 'application/json',
44
- authorization: `Bearer ${input.accessToken}`,
50
+ ...compatibilityHeaders,
51
+ authorization: `Bearer ${input.cliSessionToken}`,
45
52
  },
46
53
  signal: AbortSignal.timeout(15000),
47
54
  });
package/build/auth.js CHANGED
@@ -7,13 +7,11 @@ export async function generateAuthStatusReport() {
7
7
  readLocalConfig(),
8
8
  ]);
9
9
  return {
10
- ok: sessionState.accessToken.present &&
11
- sessionState.refreshToken.present &&
12
- apiBaseUrlState.present,
13
- accessToken: {
14
- source: sessionState.accessToken.source,
15
- present: sessionState.accessToken.present,
16
- maskedValue: maskSecret(sessionState.accessToken.value),
10
+ ok: sessionState.cliSessionToken.present && apiBaseUrlState.present,
11
+ cliSessionToken: {
12
+ source: sessionState.cliSessionToken.source,
13
+ present: sessionState.cliSessionToken.present,
14
+ maskedValue: maskSecret(sessionState.cliSessionToken.value),
17
15
  },
18
16
  apiBaseUrl: {
19
17
  source: apiBaseUrlState.source,
@@ -24,29 +22,21 @@ export async function generateAuthStatusReport() {
24
22
  path: getPostPlusConfigPath(),
25
23
  exists: configExists,
26
24
  accountId: config?.accountId?.trim() || null,
25
+ sessionExpiresAt: typeof config?.sessionExpiresAt === 'number'
26
+ ? config.sessionExpiresAt
27
+ : null,
27
28
  userEmail: typeof config?.userEmail === 'string' ? config.userEmail.trim() : null,
28
29
  userId: config?.userId?.trim() || null,
29
30
  },
30
- refreshToken: {
31
- source: sessionState.refreshToken.source,
32
- present: sessionState.refreshToken.present,
33
- maskedValue: maskSecret(sessionState.refreshToken.value),
34
- },
35
31
  };
36
32
  }
37
33
  export function formatAuthStatusReport(report) {
38
34
  const lines = ['PostPlus CLI auth status', ''];
39
- lines.push(report.accessToken.present
40
- ? `[PASS] Access token: present (${report.accessToken.source})`
41
- : '[FAIL] Access token: missing');
42
- lines.push(report.accessToken.maskedValue
43
- ? ` Value: ${report.accessToken.maskedValue}`
44
- : ' Value: not configured');
45
- lines.push(report.refreshToken.present
46
- ? `[PASS] Refresh token: present (${report.refreshToken.source})`
47
- : '[FAIL] Refresh token: missing');
48
- lines.push(report.refreshToken.maskedValue
49
- ? ` Value: ${report.refreshToken.maskedValue}`
35
+ lines.push(report.cliSessionToken.present
36
+ ? `[PASS] CLI session token: present (${report.cliSessionToken.source})`
37
+ : '[FAIL] CLI session token: missing');
38
+ lines.push(report.cliSessionToken.maskedValue
39
+ ? ` Value: ${report.cliSessionToken.maskedValue}`
50
40
  : ' Value: not configured');
51
41
  lines.push(report.apiBaseUrl.present
52
42
  ? `[PASS] PostPlus Cloud: configured (${report.apiBaseUrl.source})`
@@ -59,6 +49,9 @@ export function formatAuthStatusReport(report) {
59
49
  : `[PASS] local config path: ${report.config.path}`);
60
50
  lines.push(` Account: ${report.config.accountId ?? 'not bound'}`);
61
51
  lines.push(` User: ${report.config.userEmail ?? report.config.userId ?? 'not bound'}`);
52
+ lines.push(` Expires: ${report.config.sessionExpiresAt
53
+ ? new Date(report.config.sessionExpiresAt * 1000).toISOString()
54
+ : 'unknown'}`);
62
55
  lines.push('', report.ok ? 'Auth status OK.' : 'Auth status incomplete.');
63
56
  return lines.join('\n');
64
57
  }
package/build/doctor.js CHANGED
@@ -1,21 +1,25 @@
1
1
  import { resolveFreshRemoteAuth, } from './auth-session.js';
2
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusClientUpgradeError, } from './client-compatibility.js';
2
3
  import { resolveHostedBaseUrl } from './hosted-release.js';
3
4
  import { formatLocalDependencyReport, generateLocalDependencyReport, } from './local-dependencies.js';
4
- function createPass(id, label, detail) {
5
+ function createPass(id, label, detail, severity = 'required') {
5
6
  return {
6
7
  id,
7
8
  label,
8
9
  status: 'pass',
10
+ severity,
9
11
  detail,
10
12
  };
11
13
  }
12
- function createFail(id, label, detail, fix) {
14
+ function createFail(id, label, detail, fix, input = {}) {
13
15
  return {
14
16
  id,
15
17
  label,
16
18
  status: 'fail',
19
+ severity: input.severity ?? 'required',
17
20
  detail,
18
21
  fix,
22
+ metadata: input.metadata,
19
23
  };
20
24
  }
21
25
  export async function generateDoctorReport() {
@@ -50,7 +54,19 @@ async function checkLocalDependencies() {
50
54
  const report = await generateLocalDependencyReport();
51
55
  const detail = formatLocalDependencyReport(report);
52
56
  if (!report.ok) {
53
- return createFail('local_dependencies', 'Local dependencies', detail, 'Run the affected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing media dependencies.');
57
+ return createFail('local_dependencies', 'Task-specific local media dependencies', detail, 'Run the affected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing media dependencies.', {
58
+ severity: 'task_specific',
59
+ metadata: {
60
+ bootstrapRule: 'postplus-shared',
61
+ missingDependencies: report.checks
62
+ .filter((check) => !check.ok)
63
+ .map((check) => ({
64
+ dependency: check.dependency,
65
+ detail: check.detail,
66
+ skillIds: check.skillIds,
67
+ })),
68
+ },
69
+ });
54
70
  }
55
71
  return createPass('local_dependencies', 'Local dependencies', detail);
56
72
  }
@@ -61,9 +77,11 @@ async function checkLocalDependencies() {
61
77
  }
62
78
  }
63
79
  function buildDoctorReport(checks) {
80
+ const requiredOk = checks.every((check) => check.severity !== 'required' || check.status === 'pass');
64
81
  return {
65
82
  schemaVersion: 1,
66
83
  ok: checks.every((check) => check.status === 'pass'),
84
+ requiredOk,
67
85
  checks,
68
86
  };
69
87
  }
@@ -167,15 +185,20 @@ function readReadinessCheckFailureLabel(value) {
167
185
  : 'unknown check';
168
186
  }
169
187
  function readErrorMessage(payload, fallback) {
188
+ if (payload.code === 'postplus_client_upgrade_required') {
189
+ return formatPostPlusClientUpgradeError(payload);
190
+ }
170
191
  return typeof payload.error === 'string' && payload.error.trim().length > 0
171
192
  ? payload.error
172
193
  : fallback;
173
194
  }
174
- function requestWithAuth(input, path) {
195
+ async function requestWithAuth(input, path) {
196
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
175
197
  return fetch(`${input.apiBaseUrl}${path}`, {
176
198
  headers: {
177
199
  accept: 'application/json',
178
- authorization: `Bearer ${input.accessToken}`,
200
+ ...compatibilityHeaders,
201
+ authorization: `Bearer ${input.cliSessionToken}`,
179
202
  },
180
203
  signal: AbortSignal.timeout(15000),
181
204
  });
@@ -183,12 +206,20 @@ function requestWithAuth(input, path) {
183
206
  export function formatDoctorReport(report) {
184
207
  const lines = ['PostPlus CLI doctor', ''];
185
208
  for (const check of report.checks) {
186
- const marker = check.status === 'pass' ? '[PASS]' : '[FAIL]';
209
+ const marker = check.status === 'pass'
210
+ ? '[PASS]'
211
+ : check.severity === 'task_specific'
212
+ ? '[WARN]'
213
+ : '[FAIL]';
187
214
  lines.push(`${marker} ${check.label}: ${check.detail}`);
188
215
  if (check.fix) {
189
216
  lines.push(` Fix: ${check.fix}`);
190
217
  }
191
218
  }
192
- lines.push('', report.ok ? 'Doctor passed.' : 'Doctor failed.');
219
+ lines.push('', report.ok
220
+ ? 'Doctor passed.'
221
+ : report.requiredOk
222
+ ? 'Doctor incomplete: task-specific checks need attention.'
223
+ : 'Doctor failed.');
193
224
  return lines.join('\n');
194
225
  }
package/build/index.js CHANGED
@@ -8,6 +8,7 @@ import { assertConfigFilePermissions } from './local-state.js';
8
8
  import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
9
9
  import { runPostPlusSkillUninstall, runPostPlusSkillUpdate, } from './skill-management.js';
10
10
  import { formatStatusReport, generateStatusReport } from './status.js';
11
+ import { readCurrentCliVersion, } from './client-compatibility.js';
11
12
  import { refreshUpdateCheckCache } from './update-check.js';
12
13
  function printAuthHelp() {
13
14
  process.stdout.write(`PostPlus CLI — auth commands
@@ -41,6 +42,7 @@ Usage:
41
42
  postplus uninstall
42
43
  postplus list [--json]
43
44
  postplus status [--json]
45
+ postplus version
44
46
  postplus help
45
47
 
46
48
  Skills:
@@ -96,6 +98,10 @@ async function runList(json) {
96
98
  process.stdout.write(`${lines.join('\n')}\n`);
97
99
  return 0;
98
100
  }
101
+ async function runVersion() {
102
+ process.stdout.write(`${await readCurrentCliVersion()}\n`);
103
+ return 0;
104
+ }
99
105
  async function runSkillUpdateCommand() {
100
106
  const exitCode = await runPostPlusSkillUpdate();
101
107
  if (exitCode === 0) {
@@ -172,6 +178,11 @@ async function main() {
172
178
  printHelp();
173
179
  process.exitCode = 0;
174
180
  return;
181
+ case '--version':
182
+ case '-v':
183
+ case 'version':
184
+ process.exitCode = await runVersion();
185
+ return;
175
186
  case 'help': {
176
187
  const [helpTopic] = rest;
177
188
  if (helpTopic === 'auth') {
@@ -95,6 +95,7 @@ export async function clearLocalAuthState() {
95
95
  delete next.accessToken;
96
96
  delete next.accountId;
97
97
  delete next.apiKey;
98
+ delete next.cliSessionToken;
98
99
  delete next.machineId;
99
100
  delete next.refreshToken;
100
101
  delete next.sessionExpiresAt;
@@ -150,24 +151,19 @@ export async function setLocalApiBaseUrl(apiBaseUrl) {
150
151
  }));
151
152
  }
152
153
  export async function setLocalSession(input) {
153
- const accessToken = input.accessToken.trim();
154
- const refreshToken = input.refreshToken.trim();
154
+ const cliSessionToken = input.cliSessionToken.trim();
155
155
  const apiBaseUrl = input.apiBaseUrl.trim().replace(/\/+$/, '');
156
- if (accessToken.length === 0) {
157
- throw new Error('PostPlus CLI access token cannot be empty.');
158
- }
159
- if (refreshToken.length === 0) {
160
- throw new Error('PostPlus CLI refresh token cannot be empty.');
156
+ if (cliSessionToken.length === 0) {
157
+ throw new Error('PostPlus CLI session token cannot be empty.');
161
158
  }
162
159
  if (apiBaseUrl.length === 0) {
163
160
  throw new Error('POSTPLUS_API_BASE_URL cannot be empty.');
164
161
  }
165
162
  return updateLocalConfig((current) => ({
166
163
  ...omitLegacyAuthFields(current),
167
- accessToken,
168
164
  accountId: input.accountId,
169
165
  apiBaseUrl,
170
- refreshToken,
166
+ cliSessionToken,
171
167
  sessionExpiresAt: input.sessionExpiresAt,
172
168
  userEmail: input.userEmail,
173
169
  userId: input.userId,
@@ -182,6 +178,22 @@ export async function hasLocalConfigFile() {
182
178
  return false;
183
179
  }
184
180
  }
181
+ export async function resolveCliSessionTokenState() {
182
+ const config = await readLocalConfig();
183
+ const configValue = config?.cliSessionToken?.trim();
184
+ if (configValue && configValue.length > 0) {
185
+ return {
186
+ source: 'config',
187
+ present: true,
188
+ value: configValue,
189
+ };
190
+ }
191
+ return {
192
+ source: 'missing',
193
+ present: false,
194
+ value: null,
195
+ };
196
+ }
185
197
  export async function resolveAccessTokenState() {
186
198
  const config = await readLocalConfig();
187
199
  const configValue = config?.accessToken?.trim();
@@ -215,13 +227,9 @@ export async function resolveRefreshTokenState() {
215
227
  };
216
228
  }
217
229
  export async function resolveLocalSessionState() {
218
- const [accessToken, refreshToken] = await Promise.all([
219
- resolveAccessTokenState(),
220
- resolveRefreshTokenState(),
221
- ]);
230
+ const cliSessionToken = await resolveCliSessionTokenState();
222
231
  return {
223
- accessToken,
224
- refreshToken,
232
+ cliSessionToken,
225
233
  };
226
234
  }
227
235
  export async function resolveApiBaseUrlState() {
@@ -258,7 +266,7 @@ export function maskSecret(value) {
258
266
  return `${value.slice(0, 4)}…${value.slice(-4)}`;
259
267
  }
260
268
  function omitLegacyAuthFields(current) {
261
- const { apiKey: _apiKey, machineId: _machineId, ...rest } = (current ?? {});
269
+ const { apiKey: _apiKey, accessToken: _accessToken, machineId: _machineId, refreshToken: _refreshToken, ...rest } = (current ?? {});
262
270
  return rest;
263
271
  }
264
272
  function normalizeSkillNames(values) {
@@ -23,7 +23,8 @@ export async function loadPublicSkillCatalog(fetchFn = fetch) {
23
23
  if (!response.ok) {
24
24
  throw new Error(`Failed to load PostPlus skill catalog (${response.status}): ${response.statusText}`);
25
25
  }
26
- const payload = (await response.json());
26
+ const raw = await response.text();
27
+ const payload = parseJsonResponse(raw, POSTPLUS_SKILLS_CATALOG_URL);
27
28
  const catalog = parsePublicSkillCatalog(payload);
28
29
  return {
29
30
  ...catalog,
@@ -33,6 +34,20 @@ export async function loadPublicSkillCatalog(fetchFn = fetch) {
33
34
  listCommand: POSTPLUS_SKILLS_LIST_COMMAND,
34
35
  };
35
36
  }
37
+ function parseJsonResponse(raw, url) {
38
+ try {
39
+ return JSON.parse(raw);
40
+ }
41
+ catch (error) {
42
+ const trimmed = raw.trimStart();
43
+ if (trimmed.startsWith('<')) {
44
+ throw new Error(`PostPlus public skill catalog returned HTML instead of JSON: ${url}`);
45
+ }
46
+ throw new Error(error instanceof Error
47
+ ? `PostPlus public skill catalog returned invalid JSON: ${error.message}`
48
+ : 'PostPlus public skill catalog returned invalid JSON.');
49
+ }
50
+ }
36
51
  function parsePublicSkillCatalog(payload) {
37
52
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
38
53
  throw new Error('PostPlus public skill catalog is invalid.');
@@ -1,4 +1,5 @@
1
1
  import { runCommand, runInteractiveCommand } from './command-runner.js';
2
+ import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
2
3
  import { clearManagedSkillBaseline, readManagedSkillBaseline, writeManagedSkillBaseline, } from './local-state.js';
3
4
  import { POSTPLUS_SKILLS_AGENT_TARGETS, POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
4
5
  const NPX_SKILLS = ['-y', 'skills'];
@@ -26,6 +27,7 @@ export async function runPostPlusSkillUpdate(dependencies = {
26
27
  revision: catalog.revision,
27
28
  skillNames,
28
29
  });
30
+ await writeCurrentCliVersionToLocalConfig();
29
31
  return 0;
30
32
  }
31
33
  export async function runPostPlusSkillUninstall(dependencies = {
@@ -144,10 +146,8 @@ function mergeSkillNames(left, right) {
144
146
  return [...new Set([...left, ...right])].sort((a, b) => a.localeCompare(b));
145
147
  }
146
148
  async function listInstalledSkills(dependencies) {
147
- const [project, global] = await Promise.all([
148
- listInstalledSkillsForScope(dependencies, []),
149
- listInstalledSkillsForScope(dependencies, ['--global']),
150
- ]);
149
+ const project = await listInstalledSkillsForScope(dependencies, []);
150
+ const global = await listInstalledSkillsForScope(dependencies, ['--global']);
151
151
  const byKey = new Map();
152
152
  for (const skill of [...project, ...global]) {
153
153
  byKey.set(`${skill.scope}:${skill.name}:${skill.path}`, skill);
package/build/status.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { formatAuthStatusReport, generateAuthStatusReport, } from './auth.js';
2
+ import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
2
3
  import { formatDoctorReport, generateDoctorReport, } from './doctor.js';
3
4
  import { formatSkillInstallStatusReport, generateSkillInstallStatusReport, } from './skill-management.js';
4
5
  import { formatUpdateStatusReport, generateUpdateStatusReport, } from './update-check.js';
@@ -6,6 +7,7 @@ export async function generateStatusReport() {
6
7
  return generateStatusReportWithDependencies();
7
8
  }
8
9
  export async function generateStatusReportWithDependencies(dependencies = {}) {
10
+ await writeCurrentCliVersionToLocalConfig();
9
11
  const generateAuthStatus = dependencies.generateAuthStatus ?? generateAuthStatusReport;
10
12
  const generateDoctor = dependencies.generateDoctor ?? generateDoctorReport;
11
13
  const generateSkillStatus = dependencies.generateSkillStatus ?? generateSkillInstallStatusReport;
@@ -18,7 +20,7 @@ export async function generateStatusReportWithDependencies(dependencies = {}) {
18
20
  ]);
19
21
  return {
20
22
  schemaVersion: 1,
21
- ok: doctor.ok && auth.ok && skills.ok && updates.ok,
23
+ ok: doctor.requiredOk && auth.ok && skills.ok && updates.ok,
22
24
  doctor,
23
25
  auth,
24
26
  skills,
@@ -26,10 +28,15 @@ export async function generateStatusReportWithDependencies(dependencies = {}) {
26
28
  };
27
29
  }
28
30
  export function formatStatusReport(report) {
31
+ const taskSpecificChecksNeedAttention = report.doctor.requiredOk && !report.doctor.ok;
29
32
  return [
30
33
  'PostPlus CLI status',
31
34
  '',
32
- `Overall: ${report.ok ? 'OK' : 'INCOMPLETE'}`,
35
+ `Overall: ${report.ok
36
+ ? taskSpecificChecksNeedAttention
37
+ ? 'OK (task-specific checks need attention)'
38
+ : 'OK'
39
+ : 'INCOMPLETE'}`,
33
40
  '',
34
41
  formatDoctorReport(report.doctor),
35
42
  '',
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
+ import { readCurrentCliVersion } from './client-compatibility.js';
3
4
  import { getPostPlusConfigDir, readManagedSkillBaseline, } from './local-state.js';
4
5
  import { POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
5
6
  const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
@@ -125,15 +126,6 @@ function buildUpdateReport(input) {
125
126
  warning: null,
126
127
  };
127
128
  }
128
- async function readCurrentCliVersion() {
129
- const packageJsonPath = new URL('../package.json', import.meta.url);
130
- const raw = await readFile(packageJsonPath, 'utf8');
131
- const parsed = JSON.parse(raw);
132
- if (typeof parsed.version !== 'string' || !parsed.version.trim()) {
133
- throw new Error('Could not read the current PostPlus CLI version.');
134
- }
135
- return parsed.version.trim();
136
- }
137
129
  async function fetchLatestCliVersion(fetchFn) {
138
130
  const response = await fetchFn(NPM_LATEST_URL, {
139
131
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
5
5
  "type": "module",
6
6
  "description": "PostPlus CLI for PostPlus Cloud auth, status, and diagnostics.",