Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions bin/knowledge-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2889,15 +2889,15 @@ var init_schemas = __esm(() => {
})));
}
}

if (${id}.value === undefined) {
if (${k} in input) {
newResult[${k}] = undefined;
}
} else {
newResult[${k}] = ${id}.value;
}

`);
} else if (!isOptionalIn) {
doc.write(`
Expand Down Expand Up @@ -2934,15 +2934,15 @@ var init_schemas = __esm(() => {
path: iss.path ? [${k}, ...iss.path] : [${k}]
})));
}

if (${id}.value === undefined) {
if (${k} in input) {
newResult[${k}] = undefined;
}
} else {
newResult[${k}] = ${id}.value;
}

`);
}
}
Expand Down Expand Up @@ -14997,7 +14997,7 @@ import { existsSync as existsSync13, readFileSync as readFileSync14, writeFileSy
// package.json
var package_default = {
name: "@hasna/knowledge",
version: "0.2.79",
version: "0.2.80",
description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
type: "module",
exports: {
Expand Down Expand Up @@ -15074,7 +15074,7 @@ var package_default = {
"@ai-sdk/openai": "^3.0.68",
"@aws-sdk/client-s3": "^3.1063.0",
"@aws-sdk/credential-providers": "^3.1063.0",
"@hasna/contracts": "^0.4.1",
"@hasna/contracts": "^0.5.0",
"@hasna/events": "^0.1.3",
"@modelcontextprotocol/sdk": "^1.29.0",
"@types/json-schema": "^7.0.15",
Expand Down
402 changes: 201 additions & 201 deletions bin/knowledge.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hasna/knowledge",
"version": "0.2.79",
"version": "0.2.80",
"description": "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
"type": "module",
"exports": {
Expand Down
36 changes: 35 additions & 1 deletion src/cloud-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ import type { KnowledgeItem } from './store';
/** App slug used for the client-flip env keys (HASNA_KNOWLEDGE_*). */
export const KNOWLEDGE_APP_SLUG = 'knowledge';

/**
* Storage-mode env keys the contracts client-flip inspects, in priority order.
* Mirrors `clientTransportEnvKeys('knowledge')` in @hasna/contracts.
*/
const MODE_ENV_KEYS = [
'HASNA_KNOWLEDGE_STORAGE_MODE',
'HASNA_KNOWLEDGE_MODE',
'KNOWLEDGE_STORAGE_MODE',
'KNOWLEDGE_MODE',
] as const;
const API_URL_ENV_KEYS = ['HASNA_KNOWLEDGE_API_URL', 'KNOWLEDGE_API_URL'] as const;
const API_KEY_ENV_KEYS = ['HASNA_KNOWLEDGE_API_KEY', 'KNOWLEDGE_API_KEY'] as const;

function hasAnyEnv(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
return keys.some((k) => (env[k] ?? '').trim().length > 0);
}

/**
* The fleet flip writes exactly two vars per app —
* `HASNA_KNOWLEDGE_API_URL` + `HASNA_KNOWLEDGE_API_KEY` — and no STORAGE_MODE.
* Presence of BOTH is the self_hosted trigger. When no explicit storage-mode
* var is present we infer `cloud` so the contracts resolver routes every
* read/write to the HTTP client. An explicit storage-mode var always wins
* (e.g. `...STORAGE_MODE=local` pins local), so the cloud flip stays fully
* reversible: unset either the API URL or the API key -> local db.json.
*/
function withInferredCloudMode(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
if (hasAnyEnv(env, MODE_ENV_KEYS)) return env; // explicit mode wins
if (hasAnyEnv(env, API_URL_ENV_KEYS) && hasAnyEnv(env, API_KEY_ENV_KEYS)) {
return { ...env, HASNA_KNOWLEDGE_STORAGE_MODE: 'cloud' };
}
return env;
}

/** Cloud resource path served under /v1 by knowledge-serve. */
export const KNOWLEDGE_RESOURCE = 'notes';

Expand Down Expand Up @@ -140,7 +174,7 @@ function isNotFound(error: unknown): boolean {
* requested but misconfigured (never silent local drift).
*/
export function resolveKnowledgeCloudStore(env: NodeJS.ProcessEnv = process.env): KnowledgeCloudStore | null {
const resolved = resolveStorageClient(KNOWLEDGE_APP_SLUG, env);
const resolved = resolveStorageClient(KNOWLEDGE_APP_SLUG, withInferredCloudMode(env));
if (resolved.transport !== 'cloud-http') return null;
return wrap(resolved.client);
}
Expand Down
29 changes: 29 additions & 0 deletions tests/cloud-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ describe('knowledge cloud-store resolver (self_hosted client flip)', () => {
expect(store!.baseUrl).toBe('https://knowledge.hasna.xyz/v1');
});

test('routes to cloud when ONLY API url+key are set (fleet-flip writes no STORAGE_MODE)', () => {
// Regression: the machines flip writes exactly two vars per app
// (HASNA_KNOWLEDGE_API_URL + HASNA_KNOWLEDGE_API_KEY) and no STORAGE_MODE.
// Presence of both must trigger the cloud-http client, else the installed
// CLI silently keeps reading the local db.json even with the flip applied.
const store = resolveKnowledgeCloudStore({
HASNA_KNOWLEDGE_API_URL: 'https://knowledge.hasna.xyz',
HASNA_KNOWLEDGE_API_KEY: 'k_fake_test_key',
} as NodeJS.ProcessEnv);
expect(store).not.toBeNull();
expect(store!.baseUrl).toBe('https://knowledge.hasna.xyz/v1');
});

test('stays local when only the API url is set (key missing -> not both)', () => {
expect(
resolveKnowledgeCloudStore({
HASNA_KNOWLEDGE_API_URL: 'https://knowledge.hasna.xyz',
} as NodeJS.ProcessEnv),
).toBeNull();
});

test('stays local when only the API key is set (url missing -> not both)', () => {
expect(
resolveKnowledgeCloudStore({
HASNA_KNOWLEDGE_API_KEY: 'k_fake_test_key',
} as NodeJS.ProcessEnv),
).toBeNull();
});

test('defaults the base URL to https://knowledge.hasna.xyz/v1 when only mode+key set', () => {
const store = resolveKnowledgeCloudStore({
HASNA_KNOWLEDGE_STORAGE_MODE: 'self_hosted',
Expand Down
Loading